diff --git a/BaseButton.php b/BaseButton.php
index f144148..842c032 100644
--- a/BaseButton.php
+++ b/BaseButton.php
@@ -29,8 +29,32 @@ abstract class BaseButton extends \Nette\Application\UI\PresenterComponent
/** @var bool */
private $showText = true;
+
+ /** @var string */
+ private $enabled = true;
+ /**
+ * Is button enabled
+ * @param mixed row
+ * @return bool
+ */
+ public function isEnabled($row = null)
+ {
+ return is_bool($this->enabled) ? $this->enabled : call_user_func($this->enabled, $row);
+ }
+
+ /**
+ * Set enabled
+ * @param bool $enabled
+ * @return CheckButton
+ */
+ public function SetEnabled($enabled = true)
+ {
+ $this->enabled = $enabled;
+ return $this;
+ }
+
/**
* Get label
@@ -239,11 +263,28 @@ public function handleClick($token, $uniqueId = null)
*/
protected function createButton($row = null)
{
- return Html::el("a")
- ->href($this->getLink($row))
- ->data("gridito-icon", $this->icon)
- ->class(array("gridito-button", $this->showText ? null : "gridito-hide-text"))
- ->setText($this->label);
+ $button = Html::el('a');
+ if ($this->isEnabled($row)) {
+ $button->href($this->getLink($row));
+ } else {
+ $button->class[] = 'disabled';
+ }
+ $button->class[] = 'gridito-button';
+ if ($this->icon && $this->showText) {
+ $button->class[] = 'button-icon-text';
+ } elseif ($this->icon) {
+ $button->class[] = 'button-icon';
+ $button->class[] = 'gridito-hide-text';
+ $button->title($this->label);
+ } else {
+ $button->class[] = 'button-text';
+ }
+
+ if ($this->icon) {
+ $button->create('span')->class(array('gridito-icon', $this->icon));
+ }
+ $button->create('span class="gridito-text"')->setText($this->label);
+ return $button;
}
@@ -259,4 +300,4 @@ public function render($row = null)
}
}
-}
\ No newline at end of file
+}
diff --git a/Button.php b/Button.php
index 248de05..a9f86e0 100644
--- a/Button.php
+++ b/Button.php
@@ -52,7 +52,7 @@ public function getConfirmationQuestion($row)
if (is_callable($this->confirmationQuestion)) {
return call_user_func($this->confirmationQuestion, $row);
} else {
- return $this->confirmationQuestion;
+ return Grid::formatRecordString($row, $this->confirmationQuestion);
}
}
@@ -103,4 +103,4 @@ protected function createButton($row = null)
return $el;
}
-}
\ No newline at end of file
+}
diff --git a/CheckButton.php b/CheckButton.php
new file mode 100644
index 0000000..52f6bc8
--- /dev/null
+++ b/CheckButton.php
@@ -0,0 +1,99 @@
+checked = $checked;
+ return $this;
+ }
+
+
+ /**
+ * Is button checked
+ * @param mixed row
+ * @return bool
+ */
+ public function isChecked($row = null)
+ {
+ return is_bool($this->checked) ? $this->checked : call_user_func($this->checked, $row);
+ }
+
+
+ /**
+ * Is ajax?
+ * @return bool
+ */
+ public function isAjax()
+ {
+ return $this->ajax;
+ }
+
+
+
+ /**
+ * Set ajax mode
+ * @param bool ajax
+ * @return Button
+ */
+ public function setAjax($ajax)
+ {
+ $this->ajax = (bool) $ajax;
+ return $this;
+ }
+
+
+
+ /**
+ * Handle click signal
+ * @param string security token
+ * @param mixed primary key
+ */
+ public function handleClick($token, $uniqueId = null)
+ {
+ parent::handleClick($token, $uniqueId);
+
+ if ($this->getPresenter()->isAjax()) {
+ $this->getGrid()->invalidateControl();
+ } else {
+ $this->getGrid()->redirect("this");
+ }
+ }
+
+
+
+ /**
+ * Create button element
+ * @param mixed row
+ * @return Nette\Web\Html
+ */
+ protected function createButton($row = null)
+ {
+ $el = parent::createButton($row);
+ if ($this->isChecked($row)) {
+ $el->class[] = 'checked';
+ }
+ $el->class[] = $this->isAjax() ? $this->getGrid()->getAjaxClass() : null;
+ return $el;
+ }
+
+}
diff --git a/Column.php b/Column.php
index 03874bf..4a04cbf 100644
--- a/Column.php
+++ b/Column.php
@@ -1,6 +1,8 @@
//
public function setCellClass($class)
{
- $this->cellClass = $class;
+ $this->cellClass = $class;
return $this;
}
@@ -98,7 +115,65 @@ public function setRenderer($cellRenderer)
return $this;
}
+ /**
+ * Set maximal length of cell
+ * @param $maxlen
+ * @return Column
+ */
+ public function setLength($maxlen)
+ {
+ $this->maxlen = $maxlen;
+ return $this;
+ }
+ /**
+ * Get maximal length of cell
+ * @return int
+ */
+ public function getLength()
+ {
+ return $this->maxlen;
+ }
+
+ /**
+ * Set the type of cell
+ * @param string type
+ * @return Column
+ */
+ public function setType($type)
+ {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * Get the type of cell
+ * @return string type
+ */
+ public function getType($type)
+ {
+ return $this->type;
+ }
+
+ /**
+ * Set format of the cell
+ * @param mixed format
+ * @return Column
+ */
+ public function setFormat($format)
+ {
+ $this->format = $format;
+ return $this;
+ }
+
+ /**
+ * Get the format
+ * @return mixed
+ */
+ public function getFormat()
+ {
+ return $this->format;
+ }
/**
* Is sortable?
@@ -120,6 +195,23 @@ public function setSortable($sortable) {
return $this;
}
+ /**
+ * Set editable
+ * @param bool editable
+ * @return Column
+ */
+ public function setEditable($editable) {
+ $this->editable = $editable;
+ return $this;
+ }
+
+ /**
+ * Is editable?
+ * @return bool
+ */
+ public function isEditable() {
+ return $this->editable;
+ }
/**
@@ -177,7 +269,11 @@ public function getGrid() {
public static function renderBoolean($value)
{
$icon = $value ? "check" : "closethick";
- echo '';
+ $el = Html::el('span');
+ $el->data['value'] = $value ? '1' : '0';
+ $el->data['type'] = 'bool';
+ $el->add(Html::el("span class='ui-icon ui-icon-$icon'"));
+ return $el;
}
@@ -189,31 +285,82 @@ public static function renderBoolean($value)
*/
public static function renderDateTime($value, $format)
{
- echo $value->format($format);
+ return $value->format($format);
+ }
+
+ /**
+ * Render the text, takes care of length
+ * @param string $text text to render
+ * @param int $maxlen maximum length of text
+ */
+ public static function renderText($text, $maxlen)
+ {
+ if (is_null($maxlen) || Strings::length($text) < $maxlen) {
+ return Html::el('span')->setText($text);
+ } else {
+ return Html::el('span')->title($text)
+ ->setText(Strings::truncate($text, $maxlen));
+ }
}
+ /**
+ * Render the email address, takes care of length
+ * @param string $email email address
+ * @param int $maxlen maximum length of text
+ * @return mixed
+ */
+ public static function renderEmail($email, $maxlen)
+ {
+ if (is_null($this->spamProtection) {
+ $href = $email;
+ $text = htmlspecialchars($email, ENT_QUOTES);
+ } else {
+ $href = str_replace('@', $this->spamProtection, $email);
+ $text = str_replace('@',
+ '@'
+ . $this->spamProtection
+ . '',
+ htmlspecialchars($email, ENT_QUOTES)
+ );
+ }
+ $el = Html::el('a')->href('mailto:' . $href);
+ if (is_null($maxlen) || Strings::length($email) < $maxlen) {
+ return $el->setHtml($text);
+ } else {
+ return $el->title($href)
+ ->setText(Strings::truncate($email, $maxlen));
+ }
+ }
/**
* Default cell renderer
* @param mixed $record
* @param Column $column
+ * @return mixed
*/
public function defaultCellRenderer($record, $column) {
$name = $column->getName();
$value = $record->$name;
// boolean
- if (is_bool($value)) {
- self::renderBoolean($value);
+ if (in_array($this->type, array('bool', 'boolean')) || is_bool($value)) {
+ return self::renderBoolean($value);
// date
} elseif ($value instanceof \DateTime) {
- self::renderDateTime($value, $this->dateTimeFormat);
+ return self::renderDateTime($value, $this->dateTimeFormat);
+
+ // email
+ } elseif ($this->type == 'email') {
+ return self::renderEmail($value, $this->maxlen);
// other
} else {
- echo $value;
+ if (!is_null($this->format)) {
+ $value = Grid::formatRecordString($record, $this->format);
+ }
+ return self::renderText($value, $this->maxlen);
}
}
@@ -224,7 +371,20 @@ public function defaultCellRenderer($record, $column) {
* @param mixed record
*/
public function renderCell($record) {
- call_user_func($this->renderer ?: array($this, "defaultCellRenderer"), $record, $this);
+ $column = call_user_func($this->renderer ?: array($this, "defaultCellRenderer"), $record, $this);
+ if (!($column instanceOf Html)) {
+ $column = Html::el('span')->setText($column);
+ }
+ if ($this->editable) {
+ $column->class[] = 'editable';
+ if (!isset($column->data['value'])) {
+ $column->data['value'] = $column->getText();
+ }
+ $column->data['url'] = $this->getGrid()->link('edit!');
+ $column->data['name'] = $this->getName();
+ $column->data['id'] = $this->getGrid()->getModel()->getUniqueId($record);
+ }
+ echo $column;
}
-}
\ No newline at end of file
+}
diff --git a/Grid.php b/Grid.php
index cc7b52e..ed11a99 100644
--- a/Grid.php
+++ b/Grid.php
@@ -3,6 +3,7 @@
namespace Gridito;
use Nette\ComponentModel\Container, Nette\Environment, Nette\Utils\Paginator;
+use Nette\Utils\Strings;
/**
* Grid
@@ -23,12 +24,6 @@ class Grid extends \Nette\Application\UI\Control
/** @var int */
private $defaultItemsPerPage = 20;
- /**
- * @var int
- * @persistent
- */
- public $page = 1;
-
/**
* @var string
* @persistent
@@ -50,9 +45,9 @@ class Grid extends \Nette\Application\UI\Control
/** @var string|callable */
private $rowClass = null;
- //
+ /** @var callable */
+ private $editHandler = null;
- //
public function __construct(\Nette\ComponentModel\IContainer $parent = null, $name = null)
{
@@ -61,14 +56,22 @@ public function __construct(\Nette\ComponentModel\IContainer $parent = null, $na
$this->addComponent(new Container, "toolbar");
$this->addComponent(new Container, "actions");
$this->addComponent(new Container, "columns");
+ $this->addComponent(new \VisualPaginator, 'visualPaginator');
+ $this['visualPaginator']->onChange[] = callback($this, 'invalidateControl');
- $this->paginator = new Paginator;
+ $this->paginator = $this['visualPaginator']->getPaginator();
$this->paginator->setItemsPerPage($this->defaultItemsPerPage);
}
- //
+ public static function formatRecordString($record, $formatString)
+ {
+ return Strings::replace($formatString, '#%[^%]*%#u',
+ function ($m) use ($record) {
+ $m = Strings::trim($m[0], '%');
+ return $m != '' ? $record[$m] : "%";
+ });
+ }
- //
/**
* @param bool highlight ordered column
@@ -235,20 +238,35 @@ public function hasActions()
return count($this["actions"]->getComponents()) > 0;
}
- //
- //
-
- /**
- * Handle change page signal
- * @param int page
- */
- public function handleChangePage($page)
- {
- if ($this->presenter->isAjax()) {
- $this->invalidateControl();
- }
- }
+ /**
+ * Set edit handler
+ * @param callback handler
+ * @return Grid
+ */
+ public function setEditHandler($callback)
+ {
+ $this->editHandler = $callback;
+ return $this;
+ }
+
+ /**
+ * Handle edit
+ */
+ public function handleEdit()
+ {
+ if ($this->presenter->isAjax()) {
+ $post = \Nette\Environment::getHttpRequest()->getPost();
+ foreach ($post as $column => $value) {
+ if ($column == 'id' ||
+ $this['columns']->getComponent($column)->isEditable()) {
+ continue;
+ }
+ throw new \Nette\Application\ForbiddenRequestException("Column $column is not editable");
+ }
+ call_user_func($this->editHandler, $post);
+ }
+ }
@@ -257,11 +275,9 @@ public function handleSort($sortColumn, $sortType)
if ($this->presenter->isAjax()) {
$this->invalidateControl();
}
+ $this->paginator->page = 1;
}
- //
-
- //
/**
* Create template
@@ -279,18 +295,17 @@ protected function createTemplate($class = null)
*/
public function render()
{
- $this->paginator->setPage($this->page);
$this->model->setLimit($this->paginator->getLength());
$this->model->setOffset($this->paginator->getOffset());
if ($this->sortColumn && $this["columns"]->getComponent($this->sortColumn)->isSortable()) {
$this->model->setSorting($this->sortColumn, $this->sortType);
}
+ $this['visualPaginator']->setClass(array('paginator', $this->ajaxClass));
$this->template->render();
}
- //
/**
@@ -325,6 +340,21 @@ public function addButton($name, $label = null, array $options = array())
return $button;
}
+ /**
+ * Add check button
+ * @param string button name
+ * @param string label
+ * @param array options
+ * @return CheckButton
+ */
+ public function addCheckButton($name, $label = null, array $options = array())
+ {
+ $button = new CheckButton($this["actions"], $name);
+ $button->setLabel($label);
+ $this->setOptions($button, $options);
+ return $button;
+ }
+
/**
@@ -401,4 +431,4 @@ protected function setOptions($object, $options)
}
}
-}
\ No newline at end of file
+}
diff --git a/css/_gridito.scss b/css/_gridito.scss
new file mode 100644
index 0000000..54309a3
--- /dev/null
+++ b/css/_gridito.scss
@@ -0,0 +1,139 @@
+@import '_jquery.ui.scss';
+/**
+ * Gridito CSS
+ */
+
+/* flashes */
+div.gridito-flash {
+ padding: 0.5em;
+ margin: 1em 0;
+}
+div.gridito-flash .ui-icon {
+ float: left;
+ margin-right: 0.5em;
+}
+
+/* toolbar */
+.gridito-toolbar {
+ margin-bottom: 1em;
+}
+.gridito-toolbar .ui-button {
+ margin-right: 0.5em;
+}
+
+/* table */
+.gridito-table {
+ border-collapse: collapse;
+ width: 100%;
+ @extend .ui-widget;
+ @extend .ui-widget-content;
+ th {
+ padding: 0.5em;
+ text-align: left;
+ @extend .ui-widget-header;
+ .gridito-sorting {
+ float:right;
+ margin-left: 0.5em;
+ position: relative;
+ top: 0.2em;
+ .sorting-no, .sorting-asc, .sorting-desc {
+ @extend .ui-icon;
+ }
+ .sorting-no {
+ opacity: 0.5;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ .sorting-no, .sorting-desc:hover {
+ @extend .ui-icon-carat-2-n-s;
+ }
+ .sorting-asc, .sorting-no:hover {
+ @extend .ui-icon-triangle-1-n;
+ }
+ .sorting-desc, .sorting-asc:hover {
+ @extend .ui-icon-triangle-1-s;
+ }
+ }
+ &:hover .gridito-sorting .sorting-no {
+ opacity: 1;
+ }
+ }
+ tbody tr:hover {
+ @extend .ui-state-hover;
+ }
+}
+.gridito-table th {
+}
+.gridito-table td {
+ padding: 0.5em;
+ border: 1px solid #BBB;
+ &.highlight {
+ @extend .ui-state-highlight;
+ }
+}
+
+.gridito-table tr {
+ max-height: 2ex;
+}
+
+.gridito-button {
+ @extend .ui-button;
+ @extend .ui-widget;
+ @extend .ui-state-default;
+ @extend .ui-corner-all;
+
+ &.button-text {
+ @extend .ui-button-text-only;
+ }
+ &.button-icon {
+ @extend .ui-button-icon-only;
+ }
+ &.button-icon-text {
+ @extend .ui-button-text-icon-primary;
+ }
+ &.disabled {
+ @extend .ui-button-disabled;
+ @extend .ui-state-disabled;
+ }
+ &:hover {
+ @extend .ui-state-hover;
+ }
+ &:active, &.checked {
+ @extend .ui-state-active;
+ }
+ &:focus {
+ @extend .ui-state-focus;
+ }
+}
+span.gridito-icon {
+ @extend .ui-button-icon-primary;
+ @extend .ui-icon;
+}
+span.gridito-text {
+ @extend .ui-button-text;
+}
+span.editable {
+ width: 100%;
+ min-height: 30px;
+ line-height: 30px;
+ margin: 0px;
+ display: block;
+}
+.gridito-table .ui-state-hover .gridito-cell {
+font-weight: normal;
+}
+.gridito-actioncell .ui-button {
+margin: 0 0.5em 0 0;
+}
+.gridito-actioncell .ui-button:first-child {
+margin-left: 0;
+}
+.gridito-actioncell {
+text-align: center;
+}
+
+/* paginator */
+.gridito-paginator {
+ margin-top: 1em;
+}
diff --git a/css/gridito.css b/css/gridito.css
deleted file mode 100644
index 6591fcd..0000000
--- a/css/gridito.css
+++ /dev/null
@@ -1,66 +0,0 @@
-/**
- * Gridito CSS
- */
-
-/* flashes */
-div.gridito-flash {
- padding: 0.5em;
- margin: 1em 0;
-}
-div.gridito-flash .ui-icon {
- float: left;
- margin-right: 0.5em;
-}
-
-/* toolbar */
-.gridito-toolbar {
- margin-bottom: 1em;
-}
-.gridito-toolbar .ui-button {
- margin-right: 0.5em;
-}
-
-/* table */
-.gridito-table {
- border-collapse: collapse;
- width: 100%;
-}
-.gridito-table th {
- padding: 0.5em;
- text-align: left;
-}
-.gridito-table td {
- padding: 0.5em;
- border: 1px solid #BBB;
-}
-.gridito-table .ui-state-hover .gridito-cell {
- font-weight: normal;
-}
-.gridito-actioncell .ui-button {
- margin: 0 0.5em 0 0;
-}
-.gridito-actioncell .ui-button:first-child {
- margin-left: 0;
-}
-.gridito-actioncell {
- text-align: center;
-}
-
-/* th */
-.gridito-table th .gridito-sorting {
- float:right;
- margin-left: 0.5em;
- position: relative;
- top: 0.2em;
-}
-.gridito-table th .gridito-sorting .ui-icon {
- opacity: 0.5
-}
-.gridito-table th:hover .gridito-sorting .ui-icon, .gridito-table th .gridito-sorting .ui-icon.ui-icon-triangle-1-s, .gridito-table th .gridito-sorting .ui-icon.ui-icon-triangle-1-n {
- opacity: 1
-}
-
-/* paginator */
-.gridito-paginator {
- margin-top: 1em;
-}
\ No newline at end of file
diff --git a/js/jquery.ui.gridito.js b/js/jquery.ui.gridito.js
index f1ebe19..2938fd7 100644
--- a/js/jquery.ui.gridito.js
+++ b/js/jquery.ui.gridito.js
@@ -2,46 +2,14 @@
$.widget("ui.gridito", {
- options: {
-
- },
-
+ options: {},
_create: function () {
var _this = this;
- this.table = this.element.find("table.gridito-table");
- this.table.addClass("ui-widget ui-widget-content");
- this.table.find("th").addClass("ui-widget-header");
- this.table.find("tbody tr").hover(function () {
- $(this).addClass("ui-state-hover");
- }, function () {
- $(this).removeClass("ui-state-hover");
- });
-
- // sorting icons
- function initSortingIcons(normalClass, hoverClass) {
- _this.table.find("thead th ." + normalClass).hover(function () {
- $(this).removeClass(normalClass).addClass(hoverClass);
- }, function () {
- $(this).removeClass(hoverClass).addClass(normalClass);
- });
- };
-
- initSortingIcons("ui-icon-carat-2-n-s", "ui-icon-triangle-1-n");
- initSortingIcons("ui-icon-triangle-1-n", "ui-icon-triangle-1-s");
- initSortingIcons("ui-icon-triangle-1-s", "ui-icon-carat-2-n-s");
-
// buttons
this.element.find("a.gridito-button").each(function () {
var el = $(this);
- el.button({
- icons: {
- primary: el.attr("data-gridito-icon")
- },
- text: !el.hasClass("gridito-hide-text"),
- disabled: el.hasClass("disabled")
- });
// window button
if (el.hasClass("gridito-window-button")) {
@@ -53,7 +21,8 @@ $.widget("ui.gridito", {
win.attr("title", $(this).attr("data-gridito-window-title"));
win.load(this.href, function () {
win.dialog({
- modal: true
+ modal: true,
+ width: 600
});
win.find("input:first").focus();
});
@@ -73,4 +42,41 @@ $.widget("ui.gridito", {
});
-})(jQuery);
\ No newline at end of file
+})(jQuery);
+
+
+$.extend({
+ stopedit : function(who) {
+ var span = who.data('span');
+ span.text(who.val());
+ span.attr('data-value', who.val());
+ var data = new Object();
+ data[span.attr('data-name')] = who.val();
+ data['id'] = span.attr('data-id');
+ $.post(span.attr('data-url'), data);
+ who.replaceWith(span);
+ },
+ startedit: function(who) {
+ if (who.attr('data-type') == 'bool') {
+ var data = {};
+ data[who.attr('data-name')] = who.attr('data-value') === '1' ? '0' : '1';
+ data['id'] = who.attr('data-id');
+ $.post(who.attr('data-url'), data);
+ return;
+ }
+ var input = $('');
+ input.data('span', who);
+ input.val(who.attr('data-value'));
+ input.addClass('editable');
+ input.blur(function(){$.stopedit($(this))});
+ input.keyup(function(event)
+ {if (event.keyCode == 27) {input.blur()}}
+ );
+ who.replaceWith(input);
+ input.focus();
+ },
+});
+
+$('span.editable').live('click', function(event) {
+ $.startedit($(this));
+});
diff --git a/models/NetteModel.php b/models/NetteModel.php
new file mode 100644
index 0000000..68eb6dc
--- /dev/null
+++ b/models/NetteModel.php
@@ -0,0 +1,56 @@
+selection = $selection;
+ }
+
+ public function getItemByUniqueId($uniqueId)
+ {
+ $select = clone $this->selection;
+ return $select->where($this->getPrimaryKey(), $uniqueId)
+ ->fetch();
+ }
+
+ public function getItems()
+ {
+ $select = clone $this->selection;
+
+ list($sortColumn, $sortType) = $this->getSorting();
+ if ($sortColumn) {
+ $select->order("$sortColumn $sortType");
+ }
+ return $select->limit($this->getLimit(), $this->getOffset())
+ ->fetchPairs($this->getPrimaryKey());
+ }
+
+
+ /**
+ * Item count
+ * @return int
+ */
+ protected function _count()
+ {
+ return $this->selection->count('*');
+ }
+
+}
diff --git a/templates/grid.phtml b/templates/grid.phtml
index 0777c1e..e886b3b 100644
--- a/templates/grid.phtml
+++ b/templates/grid.phtml
@@ -13,12 +13,12 @@
{block grid}
{* flash messages *}
- {block flashes}
-
-
+ {snippet flashes}
+
+
{$flash->message}
- {/block}
+ {/snippet}
{* top toolbar *}
{block toptoolbar}
@@ -38,14 +38,13 @@
{block tableheader}
+ {var $nextSort = array('null' => 'asc', 'asc' => 'desc', 'desc' => 'null')}
|
{block tableheadercontent}
-
-
-
-
-
- {$column->getLabel()}
+ {var $next = $nextSort[$column->sorting]}
+
+ {$column->label}
+
{/block}
|
|
@@ -54,8 +53,8 @@
{block tablebody}
-
- |
+ |
+ |
{control $column:cell $item}
|
@@ -73,20 +72,9 @@
{/block}
- {block paginator}
- {var $paginator = $control->getPaginator()}
-
- Previous
-
- {for $i = 1; $i <= $paginator->pageCount; $i++}
- {$i}
- {/for}
-
- Next
-
- {/block}
+ {control visualPaginator}
{/block}
{/if}
-{/snippet}
\ No newline at end of file
+{/snippet}
|