diff --git a/module/Console/Test/View/Helper/Form/SoftwareTest.php b/module/Console/Test/View/Helper/Form/SoftwareTest.php index 6cafe998..3dcc1aee 100644 --- a/module/Console/Test/View/Helper/Form/SoftwareTest.php +++ b/module/Console/Test/View/Helper/Form/SoftwareTest.php @@ -115,73 +115,92 @@ public function testRenderSoftwareFieldset() { $view = $this->createMock('Zend\View\Renderer\PhpRenderer'); - $checkbox = $this->createMock('\Zend\Form\ElementInterface'); + $checkbox1 = $this->createMock('\Zend\Form\ElementInterface'); + $checkbox2 = $this->createMock('\Zend\Form\ElementInterface'); $fieldset = $this->createMock('Zend\Form\FieldsetInterface'); $fieldset->method('get') - ->with('c29mdHdhcmVfbmFtZQ==') // 'software_name' - ->willReturn($checkbox); + ->withConsecutive( + ['c29mdHdhcmVfbmFtZTE='], // 'software_name1' + ['c29mdHdhcmVfbmFtZTI='] // 'software_name2' + )->willReturnOnConsecutiveCalls($checkbox1, $checkbox2); $software = [ - ['name' => 'software_name', 'num_clients' => 42], + ['name' => 'software_name1', 'num_clients' => 2], + ['name' => 'software_name2', 'num_clients' => 1], ]; + $sorting = ['order' => 'current_order', 'direction' => 'current_direction']; + $formRow = $this->createMock('Zend\Form\View\Helper\FormRow'); $formRow->expects($this->at(0))->method('isTranslatorEnabled')->willReturn('translatorEnabled'); $formRow->expects($this->at(1))->method('setTranslatorEnabled')->with(false); $formRow->expects($this->at(2))->method('__invoke') - ->with($checkbox, \Zend\Form\View\Helper\FormRow::LABEL_APPEND) - ->willReturn('checkbox'); - $formRow->expects($this->at(3))->method('setTranslatorEnabled')->with('translatorEnabled'); + ->with($checkbox1, \Zend\Form\View\Helper\FormRow::LABEL_APPEND) + ->willReturn('checkbox1'); + $formRow->expects($this->at(3))->method('__invoke') + ->with($checkbox2, \Zend\Form\View\Helper\FormRow::LABEL_APPEND) + ->willReturn('checkbox2'); + $formRow->expects($this->at(4))->method('setTranslatorEnabled')->with('translatorEnabled'); $translate = $this->createMock('Zend\I18n\View\Helper\Translate'); - $translate->method('__invoke')->willReturnMap([ - ['Name', null, null, 'NAME'], - ['Count', null, null, 'COUNT'], - ]); + $translate->method('__invoke') + ->withConsecutive( + ['Name', null, null], + ['Count', null, null] + )->willReturnOnConsecutiveCalls('NAME', 'COUNT'); $consoleUrl = $this->createMock('Library\View\Helper\HtmlElement'); - $consoleUrl->method('__invoke')->with( - 'client', - 'index', - [ - 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version', - 'jumpto' => 'software', - 'filter' => 'Software', - 'search' => 'software_name', - ] - )->willReturn('url'); + $consoleUrl->method('__invoke') + ->withConsecutive( + [ + 'client', + 'index', + [ + 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version', + 'jumpto' => 'software', + 'filter' => 'Software', + 'search' => 'software_name1', + ] + ], + [ + 'client', + 'index', + [ + 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version', + 'jumpto' => 'software', + 'filter' => 'Software', + 'search' => 'software_name2', + ] + ] + )->willReturnOnConsecutiveCalls('url1', 'url2'); $htmlElement = $this->createMock('Library\View\Helper\HtmlElement'); - $htmlElement->method('__invoke')->with('a', 42, ['href' => 'url'])->willReturn('link'); + $htmlElement->method('__invoke') + ->withConsecutive( + ['a', 2, ['href' => 'url1']], + ['a', 1, ['href' => 'url2']] + )->willReturnOnConsecutiveCalls('link1', 'link2'); $table = $this->createMock('Console\View\Helper\Table'); - $table->method('__invoke') - ->with( - $software, - [ - 'name' => 'NAME', - 'num_clients' => 'COUNT' - ], - 'sorting', - $this->callback(function ($subject) use ($view, $software) { - if (count($subject) != 2) { - return false; - } - $checkbox = $subject['name']($view, $software[0]); - if ($checkbox != 'checkbox') { - printf("\nnname callback expected 'checkbox', got: %s\n", var_export($checkbox, true)); - return false; - } - $link = $subject['num_clients']($view, $software[0]); - if ($link != 'link') { - printf("\nnum_clients callback expected 'link', got: %s\n", var_export($link, true)); - return false; - } - return true; - }), - ['num_clients' => 'textright'] - )->willReturn('softwareFieldset'); + $table->method('row') + ->withConsecutive( + [ + ['name' => 'header_name', 'num_clients' => 'header_count'], + true, + [], + null, + ], + [['name' => 'checkbox1', 'num_clients' => 'link1'], false, ['num_clients' => 'textright'], null], + [['name' => 'checkbox2', 'num_clients' => 'link2'], false, ['num_clients' => 'textright'], null] + ) + ->willReturnOnConsecutiveCalls('
', '', ''); + $table->method('sortableHeader') + ->withConsecutive( + ['NAME', 'name', 'current_order', 'current_direction'], + ['COUNT', 'num_clients', 'current_order', 'current_direction'] + )->willReturnOnConsecutiveCalls('header_name', 'header_count'); + $table->method('tag')->with('
')->willReturn('softwareFieldset'); $view->method('plugin')->willReturnMap([ ['formRow', null, $formRow], @@ -199,7 +218,7 @@ public function testRenderSoftwareFieldset() $this->assertEquals( 'softwareFieldset', - $helper->renderSoftwareFieldset($fieldset, $software, 'sorting') + $helper->renderSoftwareFieldset($fieldset, $software, $sorting) ); } } diff --git a/module/Console/Test/View/Helper/TableTest.php b/module/Console/Test/View/Helper/TableTest.php index aee284d9..1cd99a86 100644 --- a/module/Console/Test/View/Helper/TableTest.php +++ b/module/Console/Test/View/Helper/TableTest.php @@ -76,52 +76,6 @@ class TableTest extends \Library\Test\View\Helper\AbstractTest ), ); - /** - * Expected result for $_headers/$_data - */ - protected $_expected = "\nHEADER1|HEADER2\nvalue1a|value2a\nvalue1b|value2b\n
\n"; - - /** - * Collection of data rendered by renderCallback() - * @var array - */ - protected $_renderCallbackData; - - /** - * Mock for row() - * - * This is a simplified row renderer. Columns are separated by "|". Rows are - * terminated by "\n". Headers are converted uppercase. - * - * @param string[] $columns - * @param bool $isHeader - * @return string - */ - public function mockRow(array $columns, $isHeader) - { - if ($isHeader) { - $columns = array_map('strtoupper', $columns); - } - return implode('|', $columns) . "\n"; - } - - /** - * Sample callback for cell rendering - * - * Cell data is returned unchanged, but also appended to - * $_renderCallbackData which can be evaluated after the table is rendered. - * - * @param \Zend\View\Renderer\RendererInterface $view - * @param array $rowData - * @param string $key - * @return mixed - */ - public function renderCallback(\Zend\View\Renderer\RendererInterface $view, array $rowData, $key) - { - $this->_renderCallbackData[] = $rowData[$key]; - return $rowData[$key]; - } - public function setUp() { $this->_escapeHtml = $this->createMock('Zend\View\Helper\EscapeHtml'); @@ -142,198 +96,210 @@ public function testInvokeNoData() $this->assertEquals('', $table(array(), $this->_headers)); } - public function testInvokeBasic() + public function testInvokeWithoutSortableHeadersDefaultParams() { - $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell - ->method('__invoke') - ->will($this->returnArgument(0)); $table = $this->getMockBuilder(static::_getHelperClass()) ->setConstructorArgs( - array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat] ) - ->setMethods(array('sortableHeader', 'row')) + ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag']) ->getMock(); - $table->expects($this->never()) // No sortable headers in this test - ->method('sortableHeader'); - $table->expects($this->exactly(3)) - ->method('row') - ->will($this->returnCallback(array($this, 'mockRow'))); - $this->assertEquals($this->_expected, $table($this->_data, $this->_headers)); + $table->expects($this->never())->method('sortableHeader'); + $table->method('row')->with($this->_headers, true, [])->willReturn('
'); + $table->method('dataRows')->with($this->_data, ['column1', 'column2'], [], [], null)->willReturn(''); + $table->method('tag')->with('
')->willReturn('tableTag'); + + $this->assertEquals('tableTag', $table($this->_data, $this->_headers)); } - public function testInvokeWithSortablHeaders() + public function testInvokeWithoutSortableHeadersExplicitParams() { - // The row() invocations are tested explicitly because the passed keys are significant. - $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell - ->method('__invoke') - ->will($this->returnArgument(0)); $table = $this->getMockBuilder(static::_getHelperClass()) ->setConstructorArgs( - array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat] ) - ->setMethods(array('sortableHeader', 'row')) + ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag']) ->getMock(); - $table->expects($this->exactly(2)) // once per column - ->method('sortableHeader') - ->will($this->returnArgument(0)); - $table->expects($this->at(2)) - ->method('row') - ->with($this->_headers, true) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(3)) - ->method('row') - ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(4)) - ->method('row') - ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false) - ->will($this->returnCallback(array($this, 'mockRow'))); + + $table->expects($this->never())->method('sortableHeader'); + $table->method('row')->with($this->_headers, true, ['columnClasses'])->willReturn('
'); + $table->method('dataRows')->with( + $this->_data, + ['column1', 'column2'], + ['renderCallbacks'], + ['columnClasses'], + ['rowClassCallback'] + )->willReturn(''); + $table->method('tag')->with('
')->willReturn('tableTag'); $this->assertEquals( - $this->_expected, - $table( - $this->_data, - $this->_headers, - array('order' => 'column1', 'direction' => 'asc') - ) + 'tableTag', + $table($this->_data, $this->_headers, [], ['renderCallbacks'], ['columnClasses'], ['rowClassCallback']) ); } - public function testInvokeWithRenderCallback() + public function testInvokeWithSortableHeaders() { - // Test with render callback on column2. - $this->_escapeHtml->expects($this->exactly(2)) // once per non-header cell that is not rendered via callback - ->method('__invoke') - ->will($this->returnArgument(0)); $table = $this->getMockBuilder(static::_getHelperClass()) ->setConstructorArgs( - array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + [$this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat] ) - ->setMethods(array('sortableHeader', 'row')) + ->setMethods(['sortableHeader', 'row', 'dataRows', 'tag']) ->getMock(); - $table->expects($this->never()) // No sortable headers in this test - ->method('sortableHeader'); - $table->expects($this->exactly(3)) - ->method('row') - ->with($this->anything(), $this->anything(), array()) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->setView($this->createMock('Zend\View\Renderer\PhpRenderer')); - - $this->_renderCallbackData = array(); + + $table->method('sortableHeader')->withConsecutive( + ['header1', 'column1', 'column2', 'desc'], + ['header2', 'column2', 'column2', 'desc'] + )->willReturnOnConsecutiveCalls('sort1', 'sort2'); + $table->method('row')->with(['column1' => 'sort1', 'column2' => 'sort2'], true, [])->willReturn('
'); + $table->method('dataRows')->with($this->_data, ['column1', 'column2'], [], [], null)->willReturn(''); + $table->method('tag')->with('
')->willReturn('tableTag'); + $this->assertEquals( - $this->_expected, - $table( - $this->_data, - $this->_headers, - array(), - array('column2' => array($this, 'renderCallback')) - ) + 'tableTag', + $table($this->_data, $this->_headers, ['order' => 'column2', 'direction' => 'desc']) ); - $this->assertEquals(array('value2a', 'value2b'), $this->_renderCallbackData); } - public function testInvokeWithColumnClasses() + public function testTag() { - // Test with column class set on column 2. The row() invocations are - // tested explicitly because the passed keys are significant. - $columnClasses = array('column2' => 'test'); - $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell - ->method('__invoke') - ->will($this->returnArgument(0)); + $this->_htmlElement->method('__invoke') + ->with('table', 'table_content', ['class' => 'alternating']) + ->willReturn('table_tag'); + + $class = static::_getHelperClass(); + $table = new $class($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat); + + $this->assertEquals('table_tag', $table->tag('table_content')); + } + + public function testDataRowsWithDefaultParams() + { + $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b'); + $table = $this->getMockBuilder(static::_getHelperClass()) ->setConstructorArgs( array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) ) - ->setMethods(array('sortableHeader', 'row')) + ->setMethods(['row']) ->getMock(); - $table->expects($this->at(0)) - ->method('row') - ->with($this->_headers, true, $columnClasses) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(1)) - ->method('row') - ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false, $columnClasses) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(2)) - ->method('row') - ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false, $columnClasses) - ->will($this->returnCallback(array($this, 'mockRow'))); - - $this->assertEquals($this->_expected, $table($this->_data, $this->_headers, array(), array(), $columnClasses)); + + $table->method('row') + ->withConsecutive( + [['column1' => '1a', 'column2' => '2a'], false, [], null], + [['column1' => '1b', 'column2' => '2b'], false, [], null] + ) + ->willReturnOnConsecutiveCalls('', ''); + + $this->assertEquals('', $table->dataRows($this->_data, ['column1', 'column2'])); } - public function testInvokeWithRowClassCallback() + public function testDataRowsWithColumnClasses() { - $rowClassCallback = function ($columns) { - static $counter = 0; - if ($counter++) { - return "$columns[column1]+$columns[column2]"; - } else { - return ''; - } - }; - $this->_escapeHtml->expects($this->exactly(4)) // once per non-header cell that is not rendered via callback - ->method('__invoke') - ->will($this->returnArgument(0)); + $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b'); + $table = $this->getMockBuilder(static::_getHelperClass()) ->setConstructorArgs( array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) ) - ->setMethods(array('sortableHeader', 'row')) + ->setMethods(['row']) ->getMock(); - $table->expects($this->never()) // No sortable headers in this test - ->method('sortableHeader'); - $table->expects($this->at(0)) - ->method('row') - ->with($this->_headers, true, array(), null) - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(1)) - ->method('row') - ->with(array('column1' => 'value1a', 'column2' => 'value2a'), false, array(), '') - ->will($this->returnCallback(array($this, 'mockRow'))); - $table->expects($this->at(2)) - ->method('row') - ->with(array('column1' => 'value1b', 'column2' => 'value2b'), false, array(), 'value1b+value2b') - ->will($this->returnCallback(array($this, 'mockRow'))); + + $table->method('row') + ->withConsecutive( + [['column1' => '1a', 'column2' => '2a'], false, ['column1' => 'class'], null], + [['column1' => '1b', 'column2' => '2b'], false, ['column1' => 'class'], null] + ) + ->willReturnOnConsecutiveCalls('', ''); $this->assertEquals( - $this->_expected, - $table( - $this->_data, - $this->_headers, - array(), - array(), - array(), - $rowClassCallback - ) + '', + $table->dataRows($this->_data, ['column1', 'column2'], [], ['column1' => 'class']) ); } - public function testDateTimeFormat() + public function testDataRowsWithRowClassCallback() { - $date = new \DateTime; - $data = array( - array(1388567012, $date), - array($date, $date), - array($date, 1388567012), - ); - $this->_dateFormat->expects($this->exactly(2)) // column 0 should be rendered by callback - ->method('__invoke') - ->with($date, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT); - $callback = function () { + $this->_escapeHtml->method('__invoke')->willReturnOnConsecutiveCalls('1a', '2a', '1b', '2b'); + + $table = $this->getMockBuilder(static::_getHelperClass()) + ->setConstructorArgs( + array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + ) + ->setMethods(['row']) + ->getMock(); + + $table->method('row') + ->withConsecutive( + [['column1' => '1a', 'column2' => '2a'], false, [], 'VALUE1A'], + [['column1' => '1b', 'column2' => '2b'], false, [], 'VALUE1B'] + ) + ->willReturnOnConsecutiveCalls('', ''); + + $rowClassCallback = function ($rowData) { + $this->assertContains($rowData, $this->_data); + return strtoupper($rowData['column1']); }; - $helper = new \Console\View\Helper\Table( - $this->_escapeHtml, - $this->_htmlElement, - $this->_consoleUrl, - $this->_dateFormat - ); - $helper( - $data, - array('col1', 'col2'), - array(), - array(0 => $callback) + + $this->assertEquals('', $table->dataRows($this->_data, ['column1', 'column2'], [], [], $rowClassCallback)); + } + + public function testDataRowsWithDateTime() + { + $date = $this->createMock('DateTime'); + + $this->_dateFormat->method('__invoke') + ->with($date, \IntlDateFormatter::SHORT, \IntlDateFormatter::SHORT) + ->willReturn('date_formatted'); + + $this->_escapeHtml->method('__invoke')->with('date_formatted')->willReturn('escaped_date'); + + $table = $this->getMockBuilder(static::_getHelperClass()) + ->setConstructorArgs( + array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + ) + ->setMethods(['row']) + ->getMock(); + + $table->method('row') + ->with(['column1' => 'escaped_date'], false, [], null) + ->willReturn(''); + + $this->assertEquals('', $table->dataRows([['column1' => $date]], ['column1'])); + } + + public function testDataRowsWithRenderCallbackPrecedesDateTime() + { + $view = $this->createMock('Zend\View\Renderer\PhpRenderer'); + $date = $this->createMock('DateTime'); + + $this->_dateFormat->expects($this->never())->method('__invoke'); + $this->_escapeHtml->expects($this->never())->method('__invoke'); + + $table = $this->getMockBuilder(static::_getHelperClass()) + ->setConstructorArgs( + array($this->_escapeHtml, $this->_htmlElement, $this->_consoleUrl, $this->_dateFormat) + ) + ->setMethods(['row', 'getView']) + ->getMock(); + + $table->method('row') + ->with(['column1' => 'callback_return'], false, [], null) + ->willReturn(''); + + $table->method('getView')->willReturn($view); + + $renderCallback = function ($view2, $rowData, $key) use ($view, $date) { + $this->assertSame($view2, $view); + $this->assertEquals(['column1' => $date], $rowData); + $this->assertEquals('column1', $key); + return 'callback_return'; + }; + + $this->assertEquals( + '', + $table->dataRows([['column1' => $date]], ['column1'], ['column1' => $renderCallback]) ); } diff --git a/module/Console/View/Helper/Form/Software.php b/module/Console/View/Helper/Form/Software.php index 73cd1da3..c3ed61bc 100644 --- a/module/Console/View/Helper/Form/Software.php +++ b/module/Console/View/Helper/Form/Software.php @@ -84,53 +84,60 @@ public function renderSoftwareFieldset($fieldset, $software, $sorting) { $view = $this->getView(); + $consoleUrl = $view->plugin('consoleUrl'); $formRow = $view->plugin('formRow'); - $translate = $view->plugin('translate'); + $htmlElement = $view->plugin('htmlElement'); $table = $view->plugin('table'); + $translate = $view->plugin('translate'); // Checkbox labels are software names and must not be translated $translatorEnabled = $formRow->isTranslatorEnabled(); $formRow->setTranslatorEnabled(false); - $output = $table( - $software, + $output = $table->row( [ - 'name' => $translate('Name'), - 'num_clients' => $translate('Count'), + 'name' => '' . $table->sortableHeader( + $translate('Name'), + 'name', + $sorting['order'], + $sorting['direction'] + ), + 'num_clients' => $table->sortableHeader( + $translate('Count'), + 'num_clients', + $sorting['order'], + $sorting['direction'] + ), ], - $sorting, - [ - 'name' => function ($view, $software) use ($fieldset, $formRow) { - $element = $fieldset->get(base64_encode($software['name'])); - return $formRow($element, \Zend\Form\View\Helper\FormRow::LABEL_APPEND); - }, - 'num_clients' => function ($view, $software) { - $htmlElement = $view->plugin('htmlElement'); - $consoleUrl = $view->plugin('consoleUrl'); - - return $htmlElement( + true + ); + foreach ($software as $row) { + $element = $fieldset->get(base64_encode($row['name'])); + $output .= $table->row( + [ + 'name' => $formRow($element, \Zend\Form\View\Helper\FormRow::LABEL_APPEND), + 'num_clients' => $htmlElement( 'a', - $software['num_clients'], - array( - 'href' => $consoleUrl( - 'client', - 'index', - array( - 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version', - 'jumpto' => 'software', - 'filter' => 'Software', - 'search' => $software['name'], - ) - ), - ), + $row['num_clients'], + ['href' => $consoleUrl( + 'client', + 'index', + [ + 'columns' => 'Name,UserName,LastContactDate,InventoryDate,Software.Version', + 'jumpto' => 'software', + 'filter' => 'Software', + 'search' => $row['name'], + ] + )], true - ); - } - ], - ['num_clients' => 'textright'] - ); + ), + ], + false, + ['num_clients' => 'textright'] + ); + } $formRow->setTranslatorEnabled($translatorEnabled); - return $output; + return $table->tag($output); } } diff --git a/module/Console/View/Helper/Table.php b/module/Console/View/Helper/Table.php index a9194377..50cbcf65 100644 --- a/module/Console/View/Helper/Table.php +++ b/module/Console/View/Helper/Table.php @@ -77,48 +77,16 @@ public function __construct( * match corresponding fields in the other arguments. For each header, a * corresponding field must be set in the table data or in $renderCallbacks. * - * $data is an array of row objects. Row objects are typically associative - * arrays or objects implementing the \ArrayAccess interface. A default - * rendering method is available for these types. For any other type, all - * columns must be rendered by a callback. If no rows are present, an - * empty string is returned. - * - * By default, cell data is retrieved from $data and escaped automatically. - * \DateTime objects are rendered as short timestamps (yy-mm-dd hh:mm). The - * application's default locale controls the date/time format. - * Alternatively, a callback can be provided in the $renderCallbacks array. - * If a callback is defined for a column, the callback is responsible for - * escaping cell data. It gets called with the following arguments: - * - * 1. The view renderer - * 2. The row object - * 3. The key of the column to be rendered. This is useful for callbacks - * that render more than 1 column. - * - * The optional $columnClasses array may contain values for a "class" - * attribute which gets applied to all cells of a specified column. The - * $columnClasses keys are matched against the keys of each row. - * - * $rowClassCallback, if given, is called for every non-header row. It - * receives the unprocessed column data for each row and delivers a string - * that is set as the row's class attribute if it is not empty. - * * If the optional $sorting array contains the "order" and "direction" - * elements (other elements are ignored), headers are generated as - * hyperlinks with "order" and "direction" parameters set to the - * corresponding column. The values denote the sorting in effect for the - * current request - the header will be marked with an arrow indicating the - * current sorting. The controller action should evaluate these parameters, - * sort the data and provide the sorting to the view renderer. The - * \Console\Mvc\Controller\Plugin\GetOrder controller plugin simplifies - * these tasks. + * elements (other elements are ignored), headers are generated via + * sortableHeader(). * - * @param array|\Traversable $data - * @param array $headers - * @param array $sorting - * @param array $renderCallbacks - * @param string[] $columnClasses Optional class attributes to apply to columns (keys are matched against $row) - * @param callable $rowClassCallback Optional callback to provide row class attributes + * @param array|\Traversable $data see dataRows() + * @param string[] $headers + * @param string[] $sorting + * @param callable[] $renderCallbacks see dataRows() + * @param string[] $columnClasses see row() + * @param callable $rowClassCallback see dataRows() * @return string HTML table */ public function __invoke( @@ -133,26 +101,68 @@ public function __invoke( return ''; } - $table = "\n"; - // Generate header row if (isset($sorting['order']) and isset($sorting['direction'])) { - $row = array(); - foreach ($headers as $key => $label) { - $row[$key] = $this->sortableHeader($label, $key, $sorting['order'], $sorting['direction']); + foreach ($headers as $key => &$label) { + $label = $this->sortableHeader($label, $key, $sorting['order'], $sorting['direction']); } - $table .= $this->row($row, true, $columnClasses); - } else { - $table .= $this->row($headers, true, $columnClasses); } + $content = $this->row($headers, true, $columnClasses); + + $content .= $this->dataRows($data, array_keys($headers), $renderCallbacks, $columnClasses, $rowClassCallback); - // Generate data rows - $keys = array_keys($headers); + return $this->tag($content); + } + + /** + * Wrap given content in "table" tag + * + * @param string $content + * @return string + */ + public function tag($content) + { + return $this->_htmlElement->__invoke('table', $content, ['class' => 'alternating']); + } + + /** + * Generate table rows + * + * $data is an array or iterator of row objects. Row objects are typically + * associative arrays or objects implementing the \ArrayAccess interface. A + * default rendering method is available for these types. For any other + * type, all columns must be rendered by a callback. + * + * The default renderer escapes cell content automatically. \DateTime + * objects are rendered as short timestamps (yy-mm-dd hh:mm). The + * application's default locale controls the date/time format. + * Alternatively, a callback can be provided in the $renderCallbacks array. + * If a callback is defined for a column, the callback is responsible for + * escaping cell data. It gets called with the following arguments: + * + * 1. The view renderer + * 2. The row object + * 3. The key of the column to be rendered. This is useful for callbacks + * that render more than 1 column. + * + * $rowClassCallback, if given, is called for each row. It receives the + * row object from $data. Its return value is passed to row(). + * + * @param array|\Traversable $data + * @param string[] $keys Column keys + * @param callable[] $renderCallbacks + * @param string $columnClasses see row() + * @param callable $rowClassCallback + * @return string + */ + public function dataRows($data, $keys, $renderCallbacks = [], $columnClasses = [], $rowClassCallback = null) + { + $rows = ''; foreach ($data as $rowData) { $row = array(); foreach ($keys as $key) { if (isset($renderCallbacks[$key])) { - $row[$key] = $renderCallbacks[$key]($this->view, $rowData, $key); + $row[$key] = $renderCallbacks[$key]($this->getView(), $rowData, $key); } elseif ($rowData[$key] instanceof \DateTime) { $row[$key] = $this->_escapeHtml->__invoke( $this->_dateFormat->__invoke( @@ -165,21 +175,25 @@ public function __invoke( $row[$key] = $this->_escapeHtml->__invoke($rowData[$key]); } } - $table .= $this->row( + $rows .= $this->row( $row, false, $columnClasses, $rowClassCallback ? $rowClassCallback($rowData) : null ); } - - $table .= "
\n"; - return $table; + return $rows; } /** * Generate a header hyperlink * + * The link URL points to the current action with the "order" and + * "direction" query parameters set accordingly. The action should evaluate + * these parameters, sort the data and provide the sorting to the view + * renderer. The \Console\Mvc\Controller\Plugin\GetOrder controller plugin + * simplifies these tasks. + * * @param string $label Header text. An arrow will be added to the currently sorted column. * @param string $key Sort key to be used in the URL * @param string $order Current order @@ -220,7 +234,7 @@ public function sortableHeader($label, $key, $order, $direction) * * @param array $columns Column data * @param bool $isHeader Use "th" tag instead of "td". Default: false - * @param string[] $columnClasses Optional class attributes to apply to cells (keys are matched against $row) + * @param string[] $columnClasses Optional class attributes to apply to cells (keys are matched against $columns) * @param string $rowClass Optional class attribute for the row * @return string HTML table row */ diff --git a/public/braintacle.js b/public/braintacle.js index 6d068667..9a6f9488 100644 --- a/public/braintacle.js +++ b/public/braintacle.js @@ -24,4 +24,9 @@ $(window).on('load', function() { }); }).change(); + + // Check/uncheck all checkboxes within the same form + $('.form_software .checkAll').change(function() { + $('input[type=checkbox][name]', this.form).prop('checked', this.checked); + }); });