diff --git a/demos/grid.php b/demos/grid.php new file mode 100644 index 0000000000..af68046d7b --- /dev/null +++ b/demos/grid.php @@ -0,0 +1,20 @@ +add(['Grid']); +$g->setModel(new Country($db)); +$g->addQuickSearch(); + +$g->menu->addItem(['Add Country', 'icon'=>'add square'], new \atk4\ui\jsExpression('alert(123)')); +$g->menu->addItem(['Re-Import', 'icon'=>'power']); +$g->menu->addItem(['Delete All', 'icon'=>'trash', 'red active']); + +$g->addAction('Say HI', new \atk4\ui\jsExpression('alert("hi")')); +$g->addAction(['icon'=>'pencil'], new \atk4\ui\jsExpression('alert($(this).closest("tr").data("id"))')); + +$sel = $g->addSelection(); +$g->menu->addItem('show selection')->on('click', new \atk4\ui\jsExpression( + 'alert("Selected: "+[].join(", "))', [$sel->jsChecked()] +)); diff --git a/demos/init.php b/demos/init.php index c0bb8e3a5f..385f4d4982 100644 --- a/demos/init.php +++ b/demos/init.php @@ -22,7 +22,7 @@ $form = $layout->leftMenu->addGroup(['Grid and Table', 'icon'=>'table']); $form->addItem('Data table with formatted columns', ['table']); $form->addItem('Table interractions', ['multitable']); - $form->addItem('Grid - Table+Bar+Search+Paginator', ['table']); + $form->addItem('Grid - Table+Bar+Search+Paginator', ['grid']); $form->addItem('Interactivity - Modals and Expanders', ['expander']); $basic = $layout->leftMenu->addGroup(['Basics', 'icon'=>'cubes']); diff --git a/docs/misc.rst b/docs/misc.rst index 5e3b27fedd..db7b9e74c1 100644 --- a/docs/misc.rst +++ b/docs/misc.rst @@ -127,7 +127,6 @@ Label has the following propetries: All the above can be string, array (passed to Icon, Image or View class) or an object. - .. php:class:: HelloWorld HelloWorld diff --git a/src/App.php b/src/App.php index 23f0fd23ec..381c8665fa 100644 --- a/src/App.php +++ b/src/App.php @@ -36,6 +36,11 @@ class App public $ui_persistence = null; + /** + * Constructor. + * + * @param array $defaults + */ public function __construct($defaults = []) { // Process defaults @@ -88,6 +93,11 @@ public function __construct($defaults = []) } } + /** + * Catch exception. + * + * @param mixed $exception + */ public function caughtException($exception) { $l = new \atk4\ui\App(); @@ -107,6 +117,11 @@ public function caughtException($exception) $this->run_called = true; } + /** + * Outputs debug info. + * + * @param string $str + */ public function outputDebug($str) { echo 'DEBUG:'.$str.'
'; @@ -116,6 +131,8 @@ public function outputDebug($str) * Will perform a preemptive output and terminate. Do not use this * directly, instead call it form Callback, jsCallback or similar * other classes. + * + * @param string $output */ public function terminate($output = null) { @@ -124,6 +141,14 @@ public function terminate($output = null) exit; } + /** + * Initializes layout. + * + * @param string|Layout\Generic $layout + * @param array $options + * + * @return $this + */ public function initLayout($layout, $options = []) { if (is_string($layout)) { @@ -154,6 +179,14 @@ public function addStyle($style) $this->html->template->appendHTML('HEAD', $this->getTag('style', $style)); } + /** + * Normalizes class name. + * + * @param string $name + * @param string $prefix + * + * @return string + */ public function normalizeClassNameApp($name, $prefix = null) { if (strpos('/', $name) === false && strpos('\\', $name) === false) { @@ -163,6 +196,11 @@ public function normalizeClassNameApp($name, $prefix = null) return $name; } + /** + * Create object and associate it with this app. + * + * @return object + */ public function add() { if ($this->layout) { @@ -180,6 +218,9 @@ public function add() } } + /** + * Runs app and echo rendered template. + */ public function run() { $this->run_called = true; @@ -201,6 +242,13 @@ public function init() $this->_init(); } + /** + * Load template. + * + * @param string $name + * + * @return Template + */ public function loadTemplate($name) { $template = new Template(); @@ -297,20 +345,49 @@ public function url($args = []) * 10. pass array as text to net tags (array must contain 1 to 3 elements corresponding to arguments): * getTag('a', ['href'=>'foo.html'], ['b','click here']); * --> click here + * + * @param string|array $tag + * @param string $attr + * @param string|array $value + * + * @return string */ public function getTag($tag = null, $attr = null, $value = null) { if ($tag === null) { $tag = 'div'; } elseif (is_array($tag)) { - $value = $attr; - $attr = $tag; + $tmp = $tag; + $tag = 'div'; + $value = ''; + + if (isset($tmp[0])) { + $tag = $tmp[0]; + unset($tmp[0]); + } + + if (isset($tmp[1])) { + $value = $tmp[1]; + unset($tmp[1]); + } + + $attr = $tmp; + } + if ($tag[0] === '<') { + return $tag; } if (is_string($attr)) { $value = $attr; $attr = null; } + + if (is_string($value)) { + $value = $this->encodeHTML($value); + } elseif (is_array($value)) { + $value = $this->getTag($value); + } + if (!$attr) { return "<$tag>".($value ? ($value)."" : ''); } diff --git a/src/Button.php b/src/Button.php index cbc921a540..f5949292e1 100644 --- a/src/Button.php +++ b/src/Button.php @@ -70,24 +70,4 @@ public function renderView() parent::renderView(); } - - /** - * Makes button into a "" element with a link. - * - * @param string $url - * - * @return $this - */ - public function link($url) - { - $this->element = 'a'; - if (is_string($url)) { - $this->setAttr('href', $url); - - return $this; - } - $this->setAttr('href', $this->app->url($url)); - - return $this; - } } diff --git a/src/Grid.php b/src/Grid.php new file mode 100644 index 0000000000..9a221e2638 --- /dev/null +++ b/src/Grid.php @@ -0,0 +1,82 @@ +menu) { + $this->menu = $this->add(['Menu', 'activate_on_click'=>false], 'Menu'); + } + + if (!$this->table) { + $this->table = $this->add(['Table', 'very compact'], 'Table'); + } + } + + public function addButton($text) + { + return $this->menu->addItem()->add(new Button($text)); + } + + public function addQuickSearch($fields = []) + { + if (!$fields) { + $fields = [$this->model->title_field]; + } + + $x = $this->menu->addMenuRight(); + $this->quickSearch = $x->addItem()->setElement('div')->add(new \atk4\ui\FormField\Input(['placeholder'=>'Search', 'icon'=>'search']))->addClass('transparent'); + } + + public function addAction($label, $action) + { + if (!$this->actions) { + $this->actions = $this->table->addColumn('TableColumn/Actions'); + } + + $this->actions->addAction($label, $action); + } + + public function setModel(\atk4\data\Model $model, $columns = null) + { + return $this->model = $this->table->setModel($model, $columns)->setLimit($this->ipp); + } + + public function addSelection() + { + $this->selection = $this->table->addColumn('TableColumn/Checkbox'); + + // Move element to the beginning + $k = array_search($this->selection, $this->table->columns); + $this->table->columns = [$k => $this->table->columns[$k]] + $this->table->columns; + + return $this->selection; + } +} diff --git a/src/Menu.php b/src/Menu.php index 44a5a9cf5b..957cb63750 100644 --- a/src/Menu.php +++ b/src/Menu.php @@ -17,11 +17,16 @@ class Menu extends View public function addItem($name = null, $action = null) { - $item = $this->add(new Item(['element'=>'a'])); - if (!is_null($name)) { - $item->set($name); + if (is_object($name)) { + $item = $name; + } elseif ($name) { + $item = new Item($name); + } else { + $item = new Item(); } + $item = $this->add($item)->setElement('a'); + if (is_array($action)) { $action = $this->app->url($action); } @@ -30,6 +35,10 @@ public function addItem($name = null, $action = null) $item->setAttr('href', $action); } + if ($action instanceof jsExpressionable) { + $item->js('click', $action); + } + return $item; } @@ -77,6 +86,14 @@ public function addGroup($title) return $group; } + public function addMenuRight() + { + $menu = $this->add(new self(), ['RightMenu', 'ui'=>false]); + $menu->removeClass('item')->addClass('right menu'); + + return $menu; + } + public function add($object, $region = null) { $item = parent::add($object, $region); diff --git a/src/Table.php b/src/Table.php index 210bd0f378..7113596d9d 100644 --- a/src/Table.php +++ b/src/Table.php @@ -104,19 +104,21 @@ class Table extends Lister * * @param string $name Data model field name * @param Column\Generic $columnDef - * @param array $fieldDef Array of defaults for new Model field * * @return Column\Generic */ - public function addColumn($name, $columnDef = null, $fieldDef = []) + public function addColumn($name, $columnDef = null) { if (!$this->model) { $this->model = new \atk4\ui\misc\ProxyModel(); } - $field = $this->model->hasElement($name); - if (!$field) { - $field = $this->model->addField($name, $fieldDef); + if ($name !== null) { + $field = $this->model->hasElement($name); + if (!$field) { + $columnDef = $name; + $name = null; + } } if ($columnDef === null) { @@ -132,7 +134,16 @@ public function addColumn($name, $columnDef = null, $fieldDef = []) } $columnDef->table = $this; - $this->columns[$name] = $columnDef; + if (is_null($name)) { + $this->columns[] = $columnDef; + } elseif (isset($this->columns[$name])) { + if (!is_array($this->columns[$name])) { + $this->columns[$name] = [$this->columns[$name]]; + } + $this->columns[$name][] = $columnDef; + } else { + $this->columns[$name] = $columnDef; + } return $columnDef; } @@ -336,9 +347,19 @@ public function renderHeaderCells() { $output = []; foreach ($this->columns as $name => $column) { - $field = $this->model->getElement($name); - $output[] = $column->getHeaderCell($field); + // If multiple formatters are defined, use the first for the header cell + if (is_array($column)) { + $column = $column[0]; + } + + if (!is_int($name)) { + $field = $this->model->getElement($name); + + $output[] = $column->getHeaderCell($field); + } else { + $output[] = $column->getHeaderCell(); + } } return implode('', $output); @@ -384,9 +405,19 @@ public function getRowTemplate() { $output = []; foreach ($this->columns as $name => $column) { - $field = $this->model->getElement($name); - $output[] = $column->getCellTemplate($field); + // If multiple formatters are defined, use the first for the header cell + if (is_array($column)) { + $column = $column[0]; + } + + if (!is_int($name)) { + $field = $this->model->getElement($name); + + $output[] = $column->getCellTemplate($field); + } else { + $output[] = $column->getCellTemplate(); + } } return implode('', $output); diff --git a/src/TableColumn/Actions.php b/src/TableColumn/Actions.php new file mode 100644 index 0000000000..ba160fa0ab --- /dev/null +++ b/src/TableColumn/Actions.php @@ -0,0 +1,40 @@ +actions) + 1); + + if (!is_object($button)) { + $button = new \atk4\ui\Button($button); + } + $button->app = $this->table->app; + + $this->actions[$name] = $button; + $button->addClass('b_'.$name); + $button->addClass('compact'); + $this->table->on('click', '.b_'.$name, $callback); + } + + public function getCellTemplate(\atk4\data\Field $f = null) + { + $output = ''; + + // render our actions + foreach ($this->actions as $action) { + $output .= $action->getHTML(); + } + + return $this->getTag('td', 'body', [$output]); + } + + // rest will be implemented for crud +} diff --git a/src/TableColumn/Checkbox.php b/src/TableColumn/Checkbox.php new file mode 100644 index 0000000000..160a7fc2cb --- /dev/null +++ b/src/TableColumn/Checkbox.php @@ -0,0 +1,45 @@ +table->jsRender().").find('.checked.".$this->class."').closest('tr').map(function(){ ". + "return $(this).data('id');}).get()"); + } + + public function init() + { + parent::init(); + if (!$this->class) { + $this->class = 'cb_'.$this->short_name; + } + } + + public function getHeaderCell(\atk4\data\Field $f = null) + { + if (isset($f)) { + throw new Exception(['Checkbox must be placed in an empty column. Don\'t specify any field.', 'field'=>$f]); + } + $this->table->js(true)->find('.'.$this->class)->checkbox(); + + return parent::getHeaderCell($f); + } + + public function getCellTemplate(\atk4\data\Field $f = null) + { + return $this->getTag('td', 'body', ['div', 'class'=>'ui checkbox '.$this->class, ['input', 'type'=>'checkbox']]); + } +} diff --git a/src/TableColumn/Generic.php b/src/TableColumn/Generic.php index fa06c0e83e..c70069fb77 100644 --- a/src/TableColumn/Generic.php +++ b/src/TableColumn/Generic.php @@ -8,6 +8,8 @@ class Generic { use \atk4\core\AppScopeTrait; + use \atk4\core\InitializerTrait; + use \atk4\core\TrackableTrait; /** * Link back to the table, where column is used. @@ -101,8 +103,12 @@ public function getTag($tag, $position, $value) * * @return string */ - public function getHeaderCell(\atk4\data\Field $f) + public function getHeaderCell(\atk4\data\Field $f = null) { + if ($f === null) { + return $this->getTag('th', 'head', ''); + } + return $this->getTag('th', 'head', $f->getCaption()); } @@ -135,8 +141,12 @@ public function getTotalsCell(\atk4\data\Field $f, $value) * * @return string */ - public function getCellTemplate(\atk4\data\Field $f) + public function getCellTemplate(\atk4\data\Field $f = null) { + if ($f === null) { + return $this->getTag('td', 'body', '{$c_'.$this->short_name.'}'); + } + return $this->getTag('td', 'body', '{$'.$f->short_name.'}'); } diff --git a/src/TableColumn/Link.php b/src/TableColumn/Link.php index 149fa6e24c..0cbda4a29b 100644 --- a/src/TableColumn/Link.php +++ b/src/TableColumn/Link.php @@ -31,15 +31,19 @@ public function __construct($page = []) /** * kill me now for this code :!! */ - public function getCellTemplate(\atk4\data\Field $f) + public function getCellTemplate(\atk4\data\Field $f = null) { + if (is_null($f)) { + $f = $this; + } + foreach ($this->page as &$val) { $val = str_replace('{$', '___o', $val); $val = str_replace('}', 'c___', $val); } $href = $this->app->url($this->page); - $output = $this->getTag('td', 'body', $this->app->getTag('a', ['href'=>$href], '{$'.$f->short_name.'}')); + $output = $this->getTag('td', 'body', ['a', 'href'=>$href, '{$'.$f->short_name.'}']); $output = str_replace('___o', '{$', $output); $output = str_replace('c___', '_urlencode}', $output); diff --git a/src/TableColumn/Money.php b/src/TableColumn/Money.php index 7276c253b2..7ca3fdbbb6 100644 --- a/src/TableColumn/Money.php +++ b/src/TableColumn/Money.php @@ -11,8 +11,12 @@ class Money extends Generic public $attr = ['all'=>['class'=>['right aligned single line']]]; - public function getCellTemplate(\atk4\data\Field $f) + public function getCellTemplate(\atk4\data\Field $f = null) { + if (!isset($f)) { + throw new Exception(['Money column requires a field']); + } + return $this->app->getTag( 'td', ['class'=> '{$_'.$f->short_name.'_money}'], diff --git a/src/TableColumn/Status.php b/src/TableColumn/Status.php index d68451f966..96a6705c62 100644 --- a/src/TableColumn/Status.php +++ b/src/TableColumn/Status.php @@ -26,13 +26,17 @@ public function __construct($states) $this->states = $states; } - public function getCellTemplate(\atk4\data\Field $f) + public function getCellTemplate(\atk4\data\Field $f = null) { + if ($f === null) { + throw new Exception(['Status can be used only with model field']); + } + return $this->app->getTag( 'td', ['class'=> '{$_'.$f->short_name.'_status}'], - $this->app->getTag('i', ['class'=>'icon {$_'.$f->short_name.'_icon}'], ''). - ' {$'.$f->short_name.'}' + [$this->app->getTag('i', ['class'=>'icon {$_'.$f->short_name.'_icon}'], ''). + ' {$'.$f->short_name.'}', ] ); } diff --git a/src/TableColumn/Template.php b/src/TableColumn/Template.php index 43d0a55d24..0ecdbec0d6 100644 --- a/src/TableColumn/Template.php +++ b/src/TableColumn/Template.php @@ -24,7 +24,7 @@ public function __construct($template) $this->template = $template; } - public function getCellTemplate(\atk4\data\Field $f) + public function getCellTemplate(\atk4\data\Field $f = null) { return $this->getTag('td', 'body', $this->template); } diff --git a/src/View.php b/src/View.php index af3fcd142d..38ccbd25eb 100644 --- a/src/View.php +++ b/src/View.php @@ -266,6 +266,40 @@ protected function setProperty($key, $val) ]); } + /** + * Sets View element. + * + * @param string $element + * + * @return $this + */ + public function setElement($element) + { + $this->element = $element; + + return $this; + } + + /** + * Makes view into a "" element with a link. + * + * @param string $url + * + * @return $this + */ + public function link($url) + { + $this->element = 'a'; + if (is_string($url)) { + $this->setAttr('href', $url); + + return $this; + } + $this->setAttr('href', $this->app->url($url)); + + return $this; + } + // }}} // {{{ Default init() method and add() logic diff --git a/template/semantic-ui/grid.html b/template/semantic-ui/grid.html new file mode 100644 index 0000000000..9bd5ed4bfd --- /dev/null +++ b/template/semantic-ui/grid.html @@ -0,0 +1,3 @@ +<{_element}div{/} id="{$_id}" class="{_ui}ui{/} {$class} {_class}{/}" style="{$style}" {$attributes}> +{$Menu} +{$Table} \ No newline at end of file diff --git a/template/semantic-ui/grid.pug b/template/semantic-ui/grid.pug new file mode 100644 index 0000000000..76432f46d5 --- /dev/null +++ b/template/semantic-ui/grid.pug @@ -0,0 +1,7 @@ +<{_element}div{/} id="{$_id}" class="{_ui}ui{/} {$class} {_class}{/}" style="{$style}" {$attributes}> + +| {$Menu} + +| {$Table} + + diff --git a/template/semantic-ui/html.html b/template/semantic-ui/html.html index 8d24a2d8d9..653b81140f 100644 --- a/template/semantic-ui/html.html +++ b/template/semantic-ui/html.html @@ -6,7 +6,7 @@ - +