$items
+ * @param $items
+ * @param string $icon
+ * @param string|null $menuId The id of the menu.
+ *
+ * @throws Exception
*
- * @return Column\JsHeaderDropdownCallback
+ * @return \atk4\ui\jsCallback
*/
- public function setHeaderDropdown($items, string $icon = 'caret square down', string $menuId = null): JsCallback
+ public function setHeaderDropdown($items, $icon = 'caret square down', $menuId = null)
{
- $this->hasHeaderAction = true;
- $id = $this->name . '_ac';
- $this->headerActionTag = ['div', ['class' => 'atk-table-dropdown'], [
+ $this->headerAction = true;
+ $id = $this->name.'_ac';
+ $this->headerActionTag = ['div', ['class'=>'atk-table-dropdown'],
[
- 'div', ['id' => $id, 'class' => 'ui top left pointing dropdown', 'data-menu-id' => $menuId],
- [['i', ['class' => $icon . ' icon'], '']],
+ [
+ 'div', ['id' => $id, 'class'=>'ui top right pointing dropdown', 'data-menu-id' => $menuId],
+ [['i', ['class' => $icon.' icon']]],
+ ],
],
- ]];
+ ];
- $cb = Column\JsHeaderDropdownCallback::addTo($this->table);
+ $cb = $this->table->add(new jsHeader());
- $function = new JsExpression('function (value, text, item) {
- if (value === undefined || value === \'\' || value === null) {
- return;
- }
- $(this).api({
- on: \'now\',
- url: \'' . $cb->getJsUrl() . '\',
- data: { item: value, id: $(this).data(\'menu-id\') }
- });
- }');
-
- $chain = new Jquery('#' . $id);
+ $function = "function(value, text, item){
+ if (value === undefined || value === '' || value === null) return;
+ $(this)
+ .api({
+ on:'now',
+ url:'{$cb->getJSURL()}',
+ data:{item:value, id:$(this).data('menu-id')}
+ }
+ );
+ }";
+
+ $chain = new jQuery('#'.$id);
$chain->dropdown([
- 'action' => 'hide',
- 'values' => $items,
- 'onChange' => $function,
- ]);
+ 'action' => 'hide',
+ 'values' => $items,
+ 'onChange' => new jsExpression($function),
+ ]);
- // will stop grid column from being sorted.
- $chain->on('click', new JsExpression('function (e) { e.stopPropagation(); }'));
+ //will stop grid column from being sorted.
+ $chain->on('click', new jsExpression('function(e){e.stopPropagation();}'));
$this->table->js(true, $chain);
@@ -229,14 +234,16 @@ public function setAttr($attr, $value, $position = 'body')
return $this;
}
- public function getTagAttributes(string $position, array $attr = []): array
+ public function getTagAttributes($position, $attr = [])
{
// "all" applies on all positions
- // $position is for specific position classes
- foreach (['all', $position] as $key) {
- if (isset($this->attr[$key])) {
- $attr = array_merge_recursive($attr, $this->attr[$key]);
- }
+ if (isset($this->attr['all'])) {
+ $attr = array_merge_recursive($attr, $this->attr['all']);
+ }
+
+ // specific position classes
+ if (isset($this->attr[$position])) {
+ $attr = array_merge_recursive($attr, $this->attr[$position]);
}
return $attr;
@@ -246,11 +253,13 @@ public function getTagAttributes(string $position, array $attr = []): array
* Returns a suitable cell tag with the supplied value. Applies modifiers
* added through addClass and setAttr.
*
- * @param string $position 'head', 'body' or 'tail'
- * @param string|array $value either HTML or array defining HTML structure, see App::getTag help
- * @param array $attr extra attributes to apply on the tag
+ * @param string $position - 'head', 'body' or 'tail'
+ * @param string $value - what is inside? either html or array defining HTML structure, see App::getTag help
+ * @param array $attr - extra attributes to apply on the tag
+ *
+ * @return string
*/
- public function getTag(string $position, $value, array $attr = []): string
+ public function getTag($position, $value, $attr = [])
{
$attr = $this->getTagAttributes($position, $attr);
@@ -258,72 +267,78 @@ public function getTag(string $position, $value, array $attr = []): string
$attr['class'] = implode(' ', $attr['class']);
}
- return $this->getApp()->getTag($position === 'body' ? 'td' : 'th', $attr, $value);
+ return $this->app->getTag($position == 'body' ? 'td' : 'th', $attr, $value);
}
/**
* Provided with a field definition (from a model) will return a header
* cell, fully formatted to be included in a Table. ().
*
- * @param mixed $value
+ * @param \atk4\data\Field $f
+ *
+ * @return string
*/
- public function getHeaderCellHtml(Field $field = null, $value = null): string
+ public function getHeaderCellHTML(\atk4\data\Field $f = null, $value = null)
{
- $tags = $this->table->hook(self::HOOK_GET_HEADER_CELL_HTML, [$this, $field, $value]);
- if ($tags) {
- return reset($tags);
+ if (!$this->table) {
+ throw new \atk4\ui\Exception(['How $table could not be set??', 'f' => $f, 'value' => $value]);
}
-
- if ($field === null) {
- return $this->getTag('head', $this->caption ?? '', $this->table->sortable ? ['class' => ['disabled']] : []);
- }
-
- // if $this->caption is empty, header caption will be overridden by linked field definition
- $caption = $this->caption ?? $field->getCaption();
-
- $attr = [
- 'data-column' => $this->columnData,
- ];
-
- $class = 'atk-table-column-header';
-
- if ($this->hasHeaderAction) {
- $attr['id'] = $this->name . '_th';
-
- // add the action tag to the caption
- $caption = [$this->headerActionTag, $caption];
+ if ($f === null) {
+ return $this->getTag('head', $this->caption ?: '', $this->table->sortable ? ['class' => ['disabled']] : []);
}
+ // If table is being sorted by THIS column, set the proper class
+ $attr = [];
if ($this->table->sortable) {
- $attr['data-sort'] = $field->shortName;
-
- if ($this->sortable) {
- $attr['class'] = ['sortable'];
- }
+ $attr['data-column'] = $f->short_name;
- // If table is being sorted by THIS column, set the proper class
- if ($this->table->sortBy === $field->shortName) {
- $class .= ' sorted ' . ['asc' => 'ascending', 'desc' => 'descending'][$this->table->sortDirection];
+ if ($this->table->sort_by === $f->short_name) {
+ $attr['class'][] = 'sorted '.$this->table->sort_order;
- if ($this->table->sortDirection === 'asc') {
- $attr['data-sort'] = '-' . $attr['data-sort'];
- } elseif ($this->table->sortDirection === 'desc') {
- $attr['data-sort'] = '';
+ if ($this->table->sort_order === 'ascending') {
+ $attr['data-column'] = '-'.$f->short_name;
+ } elseif ($this->table->sort_order === 'descending') {
+ $attr['data-column'] = '';
}
}
}
- return $this->getTag('head', [['div', ['class' => $class], $caption]], $attr);
+ if ($this->headerAction) {
+ $attr = array_merge($attr, ['id' => $this->name.'_th']);
+ $tag = $this->getTag(
+ 'head',
+ [$f->getCaption(),
+ $this->headerActionTag,
+ ],
+ $attr
+ );
+ } else {
+ $tag = $this->getTag(
+ 'head',
+ $f->getCaption(),
+ $attr
+ );
+ }
+
+ return $tag;
}
/**
* Return HTML for a total value of a specific field.
*
- * @param mixed $value
+ * @param \atk4\data\Field $f
+ * @param mixed $value
+ * @param bool $typecast Should we typecast value
+ *
+ * @return string
*/
- public function getTotalsCellHtml(Field $field, $value): string
+ public function getTotalsCellHTML(\atk4\data\Field $f, $value, $typecast = true)
{
- return $this->getTag('foot', $this->getApp()->uiPersistence->typecastSaveField($field, $value));
+ if ($typecast) {
+ $value = $this->app->ui_persistence->typecastSaveField($f, $value);
+ }
+
+ return $this->getTag('foot', $value);
}
/**
@@ -336,16 +351,20 @@ public function getTotalsCellHtml(Field $field, $value): string
* will also be formatted before inserting, see UI Persistence formatting in the documentation.
*
* This method will be executed only once per table rendering, if you need to format data manually,
- * you should use $this->table->onHook('beforeRow' or 'afterRow', ...);
+ * you should use $this->table->addHook('formatRow');
+ *
+ * @param \atk4\data\Field $f
+ *
+ * @return string
*/
- public function getDataCellHtml(Field $field = null, array $attr = []): string
+ public function getDataCellHTML(\atk4\data\Field $f = null, $extra_tags = [])
{
- return $this->getTag('body', [$this->getDataCellTemplate($field)], $attr);
+ return $this->getTag('body', [$this->getDataCellTemplate($f)], $extra_tags);
}
/**
* Provided with a field definition will return a string containing a "Template"
- * that would produce CONTENTS OF | cell when rendered. Example output:.
+ * that would produce CONTENS OF | cell when rendered. Example output:.
*
* {$name}
*
@@ -353,23 +372,30 @@ public function getDataCellHtml(Field $field = null, array $attr = []): string
* by another template returned by getDataCellTemplate when multiple formatters are
* applied to the same column. The first one to be applied is executed first, then
* a subsequent ones are executed.
+ *
+ * @param \atk4\data\Field $f
+ *
+ * @return string
*/
- public function getDataCellTemplate(Field $field = null): string
+ public function getDataCellTemplate(\atk4\data\Field $f = null)
{
- if ($field) {
- return '{$' . $field->shortName . '}';
+ if ($f) {
+ return '{$'.$f->short_name.'}';
+ } else {
+ return '{_$'.$this->short_name.'}';
}
-
- return '{_$' . $this->shortName . '}';
}
/**
* Return associative array of tags to be filled with pre-rendered HTML on
- * a column-basis. Will not be invoked if HTML output is turned off for the table.
+ * a column-basis. Will not be invoked if html-output is turned off for the table.
+ *
+ * @param array $row link to row data
+ * @param string $field field being rendered
*
- * @return array
+ * @return array Associative array with tags and their HTML values.
*/
- public function getHtmlTags(Model $row, ?Field $field): array
+ public function getHTMLTags($row, $field)
{
return [];
}
diff --git a/src/Table/Column/Link.php b/src/Table/Column/Link.php
index 058149d421..06cca99945 100644
--- a/src/Table/Column/Link.php
+++ b/src/Table/Column/Link.php
@@ -1,161 +1,119 @@
'id' ]);
+ * new Link(['order', 'id'=>'id' ]);
* or
* new Link(['order', 'id' ]);
* or
- * new Link([['order', 'x' => $myval], 'id']);.
+ * new Link([['order', 'x'=>$myval], 'id' ]);.
*/
-class Link extends Table\Column
+class Link extends Generic
{
/**
* If $url is set up, we will use pattern-matching to fill-in any part of this
* with values of the model.
*
- * @var string|HtmlTemplate
+ * @var string|array Destination definition
*/
- public $url;
+ public $url = null;
/**
* If string 'example', then will be passed to $app->url('example') along with any defined arguments.
* If set as array, then non-0 key/values will be also passed to the URL:
- * $page = ['example', 'type' => '123'];.
+ * $page = ['example', 'type'=>'123'];.
*
- * $url = $app->url(['example', 'type' => '123']);
+ * $url = $app->url(['example', 'type'=>'123']);
*
* In addition to above "args" refer to values picked up from a current row.
*
- * @var string|array<0|string, string|int|false>|null
+ * @var array
*/
- public $page;
+ public $page = null;
/**
* When constructing a URL using 'page', this specifies list of values which will be added
- * to the destination URL. For example if you set $args = ['document_id' => 'id'] then row value
- * of ['id'] will be added to URL's property "document_id".
+ * to the destination URL. For example if you set $args = ['document_id'=>'id'] then row value
+ * of ['id'] will be added to url's property "document_id".
*
* For a full example:
- * $page = ['example', 'type' => 'client'];
- * $args = ['contact_id' => 'id'];
+ * $page = ['example', 'type'=>'client'];
+ * $args = ['contact_id'=>'id'];
*
* Link URL will be "example.php?type=client&contact_id=4"
*
* You can also pass non-key arguments ['id', 'title'] and they will be added up
* as ?id=4&title=John%20Smith
*
- * @var array
+ * @var array
*/
public $args = [];
- /** @var bool use value as label of the link */
- public $useLabel = true;
-
- /** @var string|null set element class. */
- public $class;
-
- /** @var string|null Use icon as label of the link. */
- public $icon;
-
- /**
- * Set html5 target attribute in tag
- * possible values: _blank | _parent | _self | _top | frame#name.
- *
- * @var string|null
- */
- public $target;
-
- /** @var bool add download in the tag to force download from the URL. */
- public $forceDownload = false;
-
- /**
- * @param string|array<0|string, string|int|false> $page
- */
- public function __construct($page = [], array $args = [], array $defaults = [])
+ public function __construct($page = null, $args = [])
{
if (is_array($page)) {
- $defaults['page'] = $page;
- } else {
- $defaults['url'] = $page;
+ $page = ['page' => $page];
+ } elseif (is_string($page)) {
+ $page = ['url' => $page];
}
-
- $defaults['args'] = $args;
-
- parent::__construct($defaults);
+ if ($args) {
+ $page['args'] = $args;
+ }
+ parent::__construct($page);
}
- protected function init(): void
+ public function setDefaults($properties = [], $strict = false)
{
- parent::init();
-
- if (is_string($this->url)) {
- $this->url = new HtmlTemplate($this->url);
+ if (isset($properties[0])) {
+ $this->page = array_shift($properties);
}
- if (is_string($this->page)) {
- $this->page = [$this->page];
+ if (isset($properties[0])) {
+ $this->args = array_shift($properties);
}
+ parent::setDefaults($properties);
}
- public function getDataCellTemplate(Field $field = null): string
+ public function init()
{
- $attr = ['href' => '{$c_' . $this->shortName . '}'];
-
- if ($this->forceDownload) {
- $attr['download'] = 'true';
- }
-
- if ($this->target) {
- $attr['target'] = $this->target;
- }
-
- $icon = '';
- if ($this->icon) {
- $icon = $this->getApp()->getTag('i', ['class' => $this->icon . ' icon'], '');
- }
+ parent::init();
- $label = '';
- if ($this->useLabel) {
- $label = $field ? ('{$' . $field->shortName . '}') : '[Link]';
+ if ($this->url && is_string($this->url)) {
+ $this->url = new \atk4\ui\Template($this->url);
}
-
- if ($this->class) {
- $attr['class'] = $this->class;
+ if ($this->page && is_string($this->page)) {
+ $this->page = [$this->page];
}
+ }
- return $this->getApp()->getTag('a', $attr, [$icon, $label]); // TODO $label is not HTML encoded
+ public function getDataCellTemplate(\atk4\data\Field $f = null)
+ {
+ return ''.($f ? ('{$'.$f->short_name.'}') : '[Link]').'';
}
- public function getHtmlTags(Model $row, ?Field $field): array
+ public function getHTMLTags($row, $field)
{
+ // Decide on the content
if ($this->url) {
- $rowValues = $this->getApp()->uiPersistence->typecastSaveRow($row, $row->get());
-
- return ['c_' . $this->shortName => $this->url->set($rowValues)->renderToHtml()];
+ return ['c_'.$this->short_name => $this->url->set($row->get())->render()];
}
- $page = $this->page ?? [];
+ $p = $this->page ?: [];
foreach ($this->args as $key => $val) {
- if (is_int($key)) {
+ if (is_numeric($key)) {
$key = $val;
}
- $page[$key] = $row->get($val);
+ if ($row->hasElement($val)) {
+ $p[$key] = $row[$val];
+ }
}
- return ['c_' . $this->shortName => $this->table->url($page)];
+ return ['c_'.$this->short_name => $this->table->url($p)];
}
}
diff --git a/tests/TotalsTest.php b/tests/TotalsTest.php
new file mode 100644
index 0000000000..4e953afca5
--- /dev/null
+++ b/tests/TotalsTest.php
@@ -0,0 +1,105 @@
+ [
+ 1 => ['id'=>1, 'name'=>'Sock', 'type'=>'clothes', 'price'=>1, 'cnt'=>2, 'amount'=>2, 'balance'=>2],
+ 2 => ['id'=>2, 'name'=>'Hat', 'type'=>'clothes', 'price'=>5, 'cnt'=>5, 'amount'=>25, 'balance'=>27],
+ 3 => ['id'=>3, 'name'=>'Car', 'type'=>'transport', 'price'=>200, 'cnt'=>1, 'amount'=>200, 'balance'=>227],
+ 4 => ['id'=>4, 'name'=>'Bicycle', 'type'=>'transport', 'price'=>50, 'cnt'=>2, 'amount'=>100, 'balance'=>327],
+ ]];
+
+ $db = new \atk4\data\Persistence_Array($arr);
+ $m = new \atk4\data\Model($db, 'table');
+ $m->addField('name');
+ $m->addField('type');
+ $m->addField('price');
+ $m->addField('cnt');
+ $m->addField('amount');
+ $m->addField('balance');
+
+ $this->table = new \atk4\ui\Table();
+ $this->table->init();
+ $this->table->setModel($m, ['name', 'type', 'price', 'cnt', 'amount', 'balance']);
+ }
+
+ /**
+ * Test built-in totals methods.
+ */
+ public function testBuiltinRowTotals()
+ {
+ // add one totals plan to calculate built-in row totals
+ $this->table->addTotals([
+ 'name' => 'Totals:', // Totals:
+ 'type' => ['count'], // 4
+ 'price' => ['min'], // 1
+ 'cnt' => ['max'], // 5
+ 'amount' => ['sum'], // 327
+ ]);
+
+ // need to render to calculate row totals
+ $this->table->render();
+
+ // assert
+ $this->assertEquals([
+ 'type' => 4,
+ 'price' => 1,
+ 'cnt' => 5,
+ 'amount' => 327,
+ '_row_count'=> 4,
+ ], $this->table->totals[0]
+ );
+ }
+
+ /**
+ * Test advanced totals methods.
+ */
+ public function testAdvancedRowTotals()
+ {
+ // add first totals plan
+ $this->table->addTotals([
+ 'name' => 'Total {$_row_count} rows', // Total 4 rows
+ 'type' => function ($totals, $model) {
+ return 'Pay me: '.($totals['price'] * $totals['cnt']);
+ }, // 25600
+ 'price' => [
+ function ($total, $value, $model) {
+ return $total + $value;
+ },
+ ], // 256 - simple sum(price)
+ 'cnt' => [
+ function ($total, $value, $model) {
+ return max($total, $value);
+ },
+ 'default' => 100,
+ ], // 100 - uses default value max(100, max(cnt))
+ 'amount' => [
+ 'sum',
+ 'default' => function ($value, $model) {
+ return $value * 1000;
+ },
+ ], // 2327 = 2*1000 + sum(amount)
+ ], 'first');
+
+ // need to render to calculate row totals
+ $this->table->render();
+
+ // assert
+ $this->assertEquals([
+ //'type' => 25600,
+ 'price' => 256,
+ 'cnt' => 100,
+ 'amount' => 2327,
+ '_row_count'=> 4,
+ ], $this->table->totals['first']
+ );
+ }
+}
|