PageRenderTime 51ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/admincrud/extensions/bootstrap/widgets/TbExtendedGridView.php

https://github.com/max-rautkin/yii-admincrud
PHP | 1265 lines | 678 code | 127 blank | 460 comment | 90 complexity | 92340d872d4d56bd0adc7147c1b59ef9 MD5 | raw file
Possible License(s): LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /*## TbExtendedGridView class file
  3. *
  4. * @author Antonio Ramirez <antonio@clevertech.biz>
  5. * @copyright Copyright &copy; Clevertech 2012-
  6. * @license [New BSD License](http://www.opensource.org/licenses/bsd-license.php)
  7. * @package bootstrap.widgets
  8. */
  9. Yii::import('bootstrap.widgets.TbGridView');
  10. /**
  11. * TbExtendedGridView is an extended version of TbGridView.
  12. *
  13. * Features are:
  14. * - Display an extended summary of the records shown. The extended summary can be configured to any of the
  15. * {@link TbOperation} type of widgets.
  16. * - Automatic chart display (using TbHighCharts widget), where user can 'switch' between views.
  17. * - Selectable cells
  18. * - Sortable rows
  19. *
  20. * @property CActiveDataProvider $dataProvider the data provider for the view.
  21. * @property TbDataColumn[] $columns
  22. */
  23. class TbExtendedGridView extends TbGridView
  24. {
  25. /**
  26. * @var bool $fixedHeader if set to true will keep the header fixed position
  27. */
  28. public $fixedHeader = false;
  29. /**
  30. * @var integer $headerOffset, when $fixedHeader is set to true, headerOffset will position table header top position
  31. * at $headerOffset. If you are using bootstrap and has navigation top fixed, its height is 40px, so it is recommended
  32. * to use $headerOffset=40;
  33. */
  34. public $headerOffset = 0;
  35. /**
  36. * @var string the template to be used to control the layout of various sections in the view.
  37. * These tokens are recognized: {extendedSummary}, {summary}, {items} and {pager}. They will be replaced with the
  38. * extended summary, summary text, the items, and the pager.
  39. */
  40. public $template = "{summary}\n{items}\n{pager}\n{extendedSummary}";
  41. /**
  42. * @var array $extendedSummary displays an extended summary version.
  43. * There are different types of summary types,
  44. * please, see {@link TbSumOperation}, {@link TbSumOfTypeOperation},{@link TbPercentOfTypeGooglePieOperation}
  45. * {@link TbPercentOfTypeOperation} and {@link TbPercentOfTypeEasyPieOperation}.
  46. *
  47. * The following is an example, please review the different types of TbOperation classes to find out more about
  48. * its configuration parameters.
  49. *
  50. * <pre>
  51. * 'extendedSummary' => array(
  52. * 'title' => '', // the extended summary title
  53. * 'columns' => array( // the 'columns' that will be displayed at the extended summary
  54. * 'id' => array( // column name "id"
  55. * 'class' => 'TbSumOperation', // what is the type of TbOperation we are going to display
  56. * 'label' => 'Sum of Ids' // label is name of label of the resulted value (ie Sum of Ids:)
  57. * ),
  58. * 'results' => array( // column name "results"
  59. * 'class' => 'TbPercentOfTypeGooglePieOperation', // the type of TbOperation
  60. * 'label' => 'How Many Of Each? ', // the label of the operation
  61. * 'types' => array( // TbPercentOfTypeGooglePieOperation "types" attributes
  62. * '0' => array('label' => 'zeros'), // a value of "0" will be labelled "zeros"
  63. * '1' => array('label' => 'ones'), // a value of "1" will be labelled "ones"
  64. * '2' => array('label' => 'twos')) // a value of "2" will be labelled "twos"
  65. * )
  66. * )
  67. * ),
  68. * </pre>
  69. */
  70. public $extendedSummary = array();
  71. /**
  72. * @var string $extendedSummaryCssClass is the class name of the layer containing the extended summary
  73. */
  74. public $extendedSummaryCssClass = 'extended-summary';
  75. /**
  76. * @var array $extendedSummaryOptions the HTML attributes of the layer containing the extended summary
  77. */
  78. public $extendedSummaryOptions = array();
  79. /**
  80. * @var array $componentsAfterAjaxUpdate has scripts that will be executed after components have updated.
  81. * It is used internally to render scripts required for components to work correctly. You may use it for your own
  82. * scripts, just make sure it is of type array.
  83. */
  84. public $componentsAfterAjaxUpdate = array();
  85. /**
  86. * @var array $componentsReadyScripts hold scripts that will be executed on document ready.
  87. * It is used internally to render scripts required for components to work correctly. You may use it for your own
  88. * scripts, just make sure it is of type array.
  89. */
  90. public $componentsReadyScripts = array();
  91. /**
  92. * @var array $chartOptions if configured, the extended view will display a highcharts chart.
  93. */
  94. public $chartOptions = array();
  95. /**
  96. * @var bool $sortableRows. If true the rows at the table will be sortable.
  97. */
  98. public $sortableRows = false;
  99. /**
  100. * @var string Database field name for row sorting
  101. */
  102. public $sortableAttribute = 'sort_order';
  103. /**
  104. * @var boolean Save sort order by ajax defaults to false
  105. * @see bootstrap.action.TbSortableAction for an easy way to use with your controller
  106. */
  107. public $sortableAjaxSave = false;
  108. /**
  109. * @var string Name of the action to call and sort values
  110. * @see bootstrap.action.TbSortableAction for an easy way to use with your controller
  111. *
  112. * <pre>
  113. * 'sortableAction' => 'module/controller/sortable' | 'controller/sortable'
  114. * </pre>
  115. *
  116. * The widget will make use of the string to create the URL and then append $sortableAttribute
  117. * @see $sortableAttribute
  118. */
  119. public $sortableAction;
  120. /**
  121. * @var string a javascript function that will be invoked after a successful sorting is done.
  122. * The function signature is <code>function(id, position)</code> where 'id' refers to the ID of the model id key,
  123. * 'position' the new position in the list.
  124. */
  125. public $afterSortableUpdate;
  126. /**
  127. * @var bool whether to allow selecting of cells
  128. */
  129. public $selectableCells = false;
  130. /**
  131. * @var string the filter to use to allow selection. For example, if you set the "htmlOptions" property of a column to have a
  132. * "class" of "tobeselected", you could set this property as: "td.tobeselected" in order to allow selection to
  133. * those columns with that class only.
  134. */
  135. public $selectableCellsFilter = 'td';
  136. /**
  137. * @var string a javascript function that will be invoked after a selection is done.
  138. * The function signature is <code>function(selected)</code> where 'selected' refers to the selected columns.
  139. */
  140. public $afterSelectableCells;
  141. /**
  142. * @var array the configuration options to display a TbBulkActions widget
  143. * @see TbBulkActions widget for its configuration
  144. */
  145. public $bulkActions = array();
  146. /**
  147. * @var string the aligment of the bulk actions. It can be 'left' or 'right'.
  148. */
  149. public $bulkActionAlign = 'right';
  150. /**
  151. * @var TbBulkActions component that will display the bulk actions to the grid
  152. */
  153. protected $bulk;
  154. /**
  155. * @var boolean $displayExtendedSummary a helper property that is set to true if we have to render the
  156. * extended summary
  157. */
  158. protected $displayExtendedSummary;
  159. /**
  160. * @var boolean $displayChart a helper property that is set to true if we have to render a chart.
  161. */
  162. protected $displayChart;
  163. /**
  164. * @var TbOperation[] $extendedSummaryTypes hold the current configured TbOperation that will process column values.
  165. */
  166. protected $extendedSummaryTypes = array();
  167. /**
  168. * @var array $extendedSummaryOperations hold the supported operation types
  169. */
  170. protected $extendedSummaryOperations = array(
  171. 'TbSumOperation',
  172. 'TbCountOfTypeOperation',
  173. 'TbPercentOfTypeOperation',
  174. 'TbPercentOfTypeEasyPieOperation',
  175. 'TbPercentOfTypeGooglePieOperation'
  176. );
  177. /**
  178. *### .init()
  179. *
  180. * Widget initialization
  181. */
  182. public function init()
  183. {
  184. if (preg_match(
  185. '/extendedsummary/i',
  186. $this->template
  187. ) && !empty($this->extendedSummary) && isset($this->extendedSummary['columns'])
  188. ) {
  189. $this->template .= "\n{extendedSummaryContent}";
  190. $this->displayExtendedSummary = true;
  191. }
  192. if (!empty($this->chartOptions) && @$this->chartOptions['data'] && $this->dataProvider->getItemCount()) {
  193. $this->displayChart = true;
  194. }
  195. if ($this->bulkActions !== array() && isset($this->bulkActions['actionButtons'])) {
  196. if (!isset($this->bulkActions['class'])) {
  197. $this->bulkActions['class'] = 'bootstrap.widgets.TbBulkActions';
  198. }
  199. $this->bulk = Yii::createComponent($this->bulkActions, $this);
  200. $this->bulk->init();
  201. }
  202. parent::init();
  203. }
  204. /**
  205. *### .renderContent()
  206. *
  207. * Renders grid content
  208. */
  209. public function renderContent()
  210. {
  211. parent::renderContent();
  212. $this->registerCustomClientScript();
  213. }
  214. /**
  215. *### .renderKeys()
  216. *
  217. * Renders the key values of the data in a hidden tag.
  218. */
  219. public function renderKeys()
  220. {
  221. $data = $this->dataProvider->getData();
  222. if (empty($data)) {
  223. return false;
  224. }
  225. if (!$this->sortableRows || !$this->getAttribute($data[0], (string)$this->sortableAttribute)) {
  226. parent::renderKeys();
  227. }
  228. echo CHtml::openTag(
  229. 'div',
  230. array(
  231. 'class' => 'keys',
  232. 'style' => 'display:none',
  233. 'title' => Yii::app()->getRequest()->getUrl(),
  234. )
  235. );
  236. foreach ($data as $d) {
  237. echo CHtml::tag(
  238. 'span',
  239. array('data-order' => $this->getAttribute($d, $this->sortableAttribute)),
  240. CHtml::encode($this->getPrimaryKey($d))
  241. );
  242. }
  243. echo "</div>\n";
  244. return true;
  245. }
  246. /**
  247. *### .getAttribute()
  248. *
  249. * Helper function to get an attribute from the data
  250. *
  251. * @param CActiveRecord $data
  252. * @param string $attribute the attribute to get
  253. *
  254. * @return mixed the attribute value null if none found
  255. */
  256. protected function getAttribute($data, $attribute)
  257. {
  258. if ($this->dataProvider instanceof CActiveDataProvider && $data->hasAttribute($attribute)) {
  259. return $data->{$attribute};
  260. }
  261. if ($this->dataProvider instanceof CArrayDataProvider || $this->dataProvider instanceof CSqlDataProvider) {
  262. if (is_object($data) && isset($data->{$attribute})) {
  263. return $data->{$attribute};
  264. }
  265. if (isset($data[$attribute])) {
  266. return $data[$attribute];
  267. }
  268. }
  269. return null;
  270. }
  271. /**
  272. *### .getPrimaryKey()
  273. *
  274. * Helper function to return the primary key of the $data
  275. * IMPORTANT: composite keys on CActiveDataProviders will return the keys joined by comma
  276. *
  277. * @param CActiveRecord $data
  278. *
  279. * @return null|string
  280. */
  281. protected function getPrimaryKey($data)
  282. {
  283. if ($this->dataProvider instanceof CActiveDataProvider) {
  284. $key = $this->dataProvider->keyAttribute === null ? $data->getPrimaryKey() : $data->{$this->keyAttribute};
  285. return is_array($key) ? implode(',', $key) : $key;
  286. }
  287. if ($this->dataProvider instanceof CArrayDataProvider || $this->dataProvider instanceof CSqlDataProvider) {
  288. return is_object($data) ? $data->{$this->dataProvider->keyField}
  289. : $data[$this->dataProvider->keyField];
  290. }
  291. return null;
  292. }
  293. /**
  294. *### .renderTableHeader()
  295. *
  296. * Renders grid header
  297. */
  298. public function renderTableHeader()
  299. {
  300. $this->renderChart();
  301. parent::renderTableHeader();
  302. }
  303. /**
  304. *### .renderTableFooter()
  305. *
  306. * Renders the table footer.
  307. */
  308. public function renderTableFooter()
  309. {
  310. $hasFilter = $this->filter !== null && $this->filterPosition === self::FILTER_POS_FOOTER;
  311. $hasFooter = $this->getHasFooter();
  312. if ($this->bulk !== null || $hasFilter || $hasFooter) {
  313. echo "<tfoot>\n";
  314. if ($hasFooter) {
  315. echo "<tr>\n";
  316. /** @var $column CDataColumn */
  317. foreach ($this->columns as $column) {
  318. $column->renderFooterCell();
  319. }
  320. echo "</tr>\n";
  321. }
  322. if ($hasFilter) {
  323. $this->renderFilter();
  324. }
  325. if ($this->bulk !== null) {
  326. $this->renderBulkActions();
  327. }
  328. echo "</tfoot>\n";
  329. }
  330. }
  331. /**
  332. *### .renderBulkActions()
  333. */
  334. public function renderBulkActions()
  335. {
  336. echo '<tr><td colspan="' . count($this->columns) . '">';
  337. $this->bulk->renderButtons();
  338. echo '</td></tr>';
  339. }
  340. /**
  341. *### .renderChart()
  342. *
  343. * Renders chart
  344. * @throws CException
  345. */
  346. public function renderChart()
  347. {
  348. if (!$this->displayChart || $this->dataProvider->getItemCount() <= 0) {
  349. return;
  350. }
  351. if (!isset($this->chartOptions['data']['series'])) {
  352. throw new CException(Yii::t(
  353. 'zii',
  354. 'You need to set the "series" attribute in order to render a chart'
  355. ));
  356. }
  357. $configSeries = $this->chartOptions['data']['series'];
  358. if (!is_array($configSeries)) {
  359. throw new CException(Yii::t('zii', '"chartOptions.series" is expected to be an array.'));
  360. }
  361. if (!isset($this->chartOptions['config'])) {
  362. $this->chartOptions['config'] = array();
  363. }
  364. // ****************************************
  365. // render switch buttons
  366. $buttons = Yii::createComponent(
  367. array(
  368. 'class' => 'bootstrap.widgets.TbButtonGroup',
  369. 'toggle' => 'radio',
  370. 'buttons' => array(
  371. array(
  372. 'label' => Yii::t('zii', 'Grid'),
  373. 'url' => '#',
  374. 'htmlOptions' => array('class' => 'active ' . $this->getId() . '-grid-control grid')
  375. ),
  376. array(
  377. 'label' => Yii::t('zii', 'Chart'),
  378. 'url' => '#',
  379. 'htmlOptions' => array('class' => $this->getId() . '-grid-control chart')
  380. ),
  381. ),
  382. 'htmlOptions' => array('style' => 'margin-bottom:5px')
  383. )
  384. );
  385. echo '<div class="row">';
  386. $buttons->init();
  387. $buttons->run();
  388. echo '</div>';
  389. $chartId = preg_replace('[-\\ ?]', '_', 'exgvwChart' . $this->getId()); // cleaning out most possible characters invalid as javascript variable identifiers.
  390. $this->componentsReadyScripts[] = '$(document).on("click",".' . $this->getId() . '-grid-control", function(){
  391. if ($(this).hasClass("grid") && $("#' . $this->getId() . ' #' . $chartId . '").is(":visible"))
  392. {
  393. $("#' . $this->getId() . ' #' . $chartId . '").hide();
  394. $("#' . $this->getId() . ' table.items").show();
  395. }
  396. if ($(this).hasClass("chart") && $("#' . $this->getId() . ' table.items").is(":visible"))
  397. {
  398. $("#' . $this->getId() . ' table.items").hide();
  399. $("#' . $this->getId() . ' #' . $chartId . '").show();
  400. }
  401. return false;
  402. });';
  403. // end switch buttons
  404. // ****************************************
  405. // render Chart
  406. // chart options
  407. $data = $this->dataProvider->getData();
  408. $count = count($data);
  409. $seriesData = array();
  410. $cnt = 0;
  411. foreach ($configSeries as $set) {
  412. $seriesData[$cnt] = array('name' => isset($set['name']) ? $set['name'] : null, 'data' => array());
  413. for ($row = 0; $row < $count; ++$row) {
  414. $column = $this->getColumnByName($set['attribute']);
  415. if (!is_null($column) && $column->value !== null) {
  416. $seriesData[$cnt]['data'][] = $this->evaluateExpression(
  417. $column->value,
  418. array('data' => $data[$row], 'row' => $row)
  419. );
  420. } else {
  421. $value = CHtml::value($data[$row], $set['attribute']);
  422. $seriesData[$cnt]['data'][] = is_numeric($value) ? (float)$value : $value;
  423. }
  424. }
  425. ++$cnt;
  426. }
  427. // ****************************************
  428. // render chart
  429. $options = CMap::mergeArray(
  430. $this->chartOptions['config'],
  431. array('series' => $seriesData)
  432. );
  433. $this->chartOptions['htmlOptions'] = isset($this->chartOptions['htmlOptions'])
  434. ? $this->chartOptions['htmlOptions'] : array();
  435. $this->chartOptions['htmlOptions']['style'] = 'display:none'; // sorry but use a class to provide styles, we need this
  436. // build unique ID
  437. // important!
  438. echo '<div class="row">';
  439. if ($this->ajaxUpdate !== false) {
  440. if (isset($options['chart']) && is_array($options['chart'])) {
  441. $options['chart']['renderTo'] = $chartId;
  442. } else {
  443. $options['chart'] = array('renderTo' => $chartId);
  444. }
  445. $jsOptions = CJSON::encode($options);
  446. if (isset($this->chartOptions['htmlOptions']['data-config'])) {
  447. unset($this->chartOptions['htmlOptions']['data-config']);
  448. }
  449. echo "<div id='{$chartId}' " . CHtml::renderAttributes(
  450. $this->chartOptions['htmlOptions']
  451. ) . " data-config='{$jsOptions}'></div>";
  452. $this->componentsAfterAjaxUpdate[] = "highchart{$chartId} = new Highcharts.Chart($('#{$chartId}').data('config'));";
  453. }
  454. $configChart = array(
  455. 'class' => 'bootstrap.widgets.TbHighCharts',
  456. 'id' => $chartId,
  457. 'options' => $options,
  458. 'htmlOptions' => $this->chartOptions['htmlOptions']
  459. );
  460. $chart = Yii::createComponent($configChart);
  461. $chart->init();
  462. $chart->run();
  463. echo '</div>';
  464. // end chart display
  465. // ****************************************
  466. }
  467. /**
  468. *### .renderTableRow()
  469. *
  470. * Renders a table body row.
  471. *
  472. * @param integer $row the row number (zero-based).
  473. */
  474. public function renderTableRow($row)
  475. {
  476. $htmlOptions = array();
  477. if ($this->rowHtmlOptionsExpression !== null) {
  478. $data = $this->dataProvider->data[$row];
  479. $options = $this->evaluateExpression(
  480. $this->rowHtmlOptionsExpression,
  481. array('row' => $row, 'data' => $data)
  482. );
  483. if (is_array($options)) {
  484. $htmlOptions = $options;
  485. }
  486. }
  487. if ($this->rowCssClassExpression !== null) {
  488. $data = $this->dataProvider->data[$row];
  489. $class = $this->evaluateExpression($this->rowCssClassExpression, array('row' => $row, 'data' => $data));
  490. } elseif (is_array($this->rowCssClass) && ($n = count($this->rowCssClass)) > 0) {
  491. $class = $this->rowCssClass[$row % $n];
  492. }
  493. if (!empty($class)) {
  494. if (isset($htmlOptions['class'])) {
  495. $htmlOptions['class'] .= ' ' . $class;
  496. } else {
  497. $htmlOptions['class'] = $class;
  498. }
  499. }
  500. echo CHtml::openTag('tr', $htmlOptions) . "\n";
  501. foreach ($this->columns as $column) {
  502. echo $this->displayExtendedSummary && !empty($this->extendedSummary['columns']) ? $this->parseColumnValue(
  503. $column,
  504. $row
  505. ) : $column->renderDataCell($row);
  506. }
  507. echo "</tr>\n";
  508. }
  509. /**
  510. *### .renderExtendedSummary()
  511. *
  512. * Renders summary
  513. */
  514. public function renderExtendedSummary()
  515. {
  516. if (!isset($this->extendedSummaryOptions['class'])) {
  517. $this->extendedSummaryOptions['class'] = $this->extendedSummaryCssClass;
  518. } else {
  519. $this->extendedSummaryOptions['class'] .= ' ' . $this->extendedSummaryCssClass;
  520. }
  521. echo '<div ' . CHtml::renderAttributes($this->extendedSummaryOptions) . '></div>';
  522. }
  523. /**
  524. *### .renderExtendedSummaryContent()
  525. *
  526. * Renders summary content. Will be appended to
  527. */
  528. public function renderExtendedSummaryContent()
  529. {
  530. if (($count = $this->dataProvider->getItemCount()) <= 0) {
  531. return;
  532. }
  533. if (!empty($this->extendedSummaryTypes)) {
  534. echo '<div id="' . $this->id . '-extended-summary" style="display:none">';
  535. if (isset($this->extendedSummary['title'])) {
  536. echo '<h3>' . $this->extendedSummary['title'] . '</h3>';
  537. }
  538. foreach ($this->extendedSummaryTypes as $summaryType) {
  539. /** @var $summaryType TbOperation */
  540. $summaryType->run();
  541. echo '<br/>';
  542. }
  543. echo '</div>';
  544. }
  545. }
  546. /**
  547. *### .registerCustomClientScript()
  548. *
  549. * This script must be run at the end of content rendering not at the beginning as it is common with normal CGridViews
  550. */
  551. public function registerCustomClientScript()
  552. {
  553. /** @var $cs CClientScript */
  554. $cs = Yii::app()->getClientScript();
  555. $fixedHeaderJs = '';
  556. if ($this->fixedHeader) {
  557. Yii::app()->bootstrap->registerAssetJs('jquery.stickytableheaders' . (!YII_DEBUG ? '.min' : '') . '.js');
  558. $fixedHeaderJs = "$('#{$this->id} table.items').stickyTableHeaders({fixedOffset:{$this->headerOffset}});";
  559. $this->componentsAfterAjaxUpdate[] = $fixedHeaderJs;
  560. }
  561. if ($this->sortableRows) {
  562. $afterSortableUpdate = '';
  563. if ($this->afterSortableUpdate !== null) {
  564. if (!($this->afterSortableUpdate instanceof CJavaScriptExpression) && strpos(
  565. $this->afterSortableUpdate,
  566. 'js:'
  567. ) !== 0
  568. ) {
  569. $afterSortableUpdate = new CJavaScriptExpression($this->afterSortableUpdate);
  570. } else {
  571. $afterSortableUpdate = $this->afterSortableUpdate;
  572. }
  573. }
  574. $this->selectableRows = 1;
  575. $cs->registerCoreScript('jquery.ui');
  576. Yii::app()->bootstrap->registerAssetJs('jquery.sortable.gridview.js');
  577. if ($this->sortableAjaxSave && $this->sortableAction !== null) {
  578. $sortableAction = Yii::app()->createUrl(
  579. $this->sortableAction,
  580. array('sortableAttribute' => $this->sortableAttribute)
  581. );
  582. } else {
  583. $sortableAction = '';
  584. }
  585. $afterSortableUpdate = CJavaScript::encode($afterSortableUpdate);
  586. $this->componentsReadyScripts[] = "$.fn.yiiGridView.sortable('{$this->id}', '{$sortableAction}', {$afterSortableUpdate});";
  587. $this->componentsAfterAjaxUpdate[] = "$.fn.yiiGridView.sortable('{$this->id}', '{$sortableAction}', {$afterSortableUpdate});";
  588. }
  589. if ($this->selectableCells) {
  590. $afterSelectableCells = '';
  591. if ($this->afterSelectableCells !== null) {
  592. echo strpos($this->afterSelectableCells, 'js:');
  593. if (!($this->afterSelectableCells instanceof CJavaScriptExpression) && strpos(
  594. $this->afterSelectableCells,
  595. 'js:'
  596. ) !== 0
  597. ) {
  598. $afterSelectableCells = new CJavaScriptExpression($this->afterSelectableCells);
  599. } else {
  600. $afterSelectableCells = $this->afterSelectableCells;
  601. }
  602. }
  603. $cs->registerCoreScript('jquery.ui');
  604. Yii::app()->bootstrap->registerAssetJs('jquery.selectable.gridview.js');
  605. $afterSelectableCells = CJavaScript::encode($afterSelectableCells);
  606. $this->componentsReadyScripts[] = "$.fn.yiiGridView.selectable('{$this->id}','{$this->selectableCellsFilter}',{$afterSelectableCells});";
  607. $this->componentsAfterAjaxUpdate[] = "$.fn.yiiGridView.selectable('{$this->id}','{$this->selectableCellsFilter}', {$afterSelectableCells});";
  608. }
  609. $cs->registerScript(
  610. __CLASS__ . '#' . $this->id . 'Ex',
  611. '
  612. $grid = $("#' . $this->id . '");
  613. ' . $fixedHeaderJs . '
  614. if ($(".' . $this->extendedSummaryCssClass . '", $grid).length)
  615. {
  616. $(".' . $this->extendedSummaryCssClass . '", $grid).html($("#' . $this->id . '-extended-summary", $grid).html());
  617. }
  618. ' . (count($this->componentsReadyScripts) ? implode(PHP_EOL, $this->componentsReadyScripts) : '') . '
  619. $.ajaxPrefilter(function (options, originalOptions, jqXHR) {
  620. var qs = $.deparam.querystring(options.url);
  621. if (qs.hasOwnProperty("ajax") && qs.ajax == "' . $this->id . '")
  622. {
  623. options.realsuccess = options.success;
  624. options.success = function(data)
  625. {
  626. if (options.realsuccess) {
  627. options.realsuccess(data);
  628. var $data = $("<div>" + data + "</div>");
  629. // we need to get the grid again... as it has been updated
  630. if ($(".' . $this->extendedSummaryCssClass . '", $("#' . $this->id . '")))
  631. {
  632. $(".' . $this->extendedSummaryCssClass . '", $("#' . $this->id . '")).html($("#' . $this->id . '-extended-summary", $data).html());
  633. }
  634. ' . (count($this->componentsAfterAjaxUpdate) ? implode(
  635. PHP_EOL,
  636. $this->componentsAfterAjaxUpdate
  637. ) : '') . '
  638. }
  639. }
  640. }
  641. });'
  642. );
  643. }
  644. /**
  645. *### .parseColumnValue()
  646. *
  647. * @param CDataColumn $column
  648. * @param integer $row the current row number
  649. *
  650. * @return string
  651. */
  652. protected function parseColumnValue($column, $row)
  653. {
  654. ob_start();
  655. $column->renderDataCell($row);
  656. $value = ob_get_clean();
  657. if ($column instanceof CDataColumn && array_key_exists($column->name, $this->extendedSummary['columns'])) {
  658. // lets get the configuration
  659. $config = $this->extendedSummary['columns'][$column->name];
  660. // add the required column object in
  661. $config['column'] = $column;
  662. // build the summary operation object
  663. $op = $this->getSummaryOperationInstance($column->name, $config);
  664. // process the value
  665. $op->processValue($value);
  666. }
  667. return $value;
  668. }
  669. /**
  670. *### .getSummaryOperationInstance()
  671. *
  672. * Each type of 'extended' summary
  673. *
  674. * @param string $name the name of the column
  675. * @param array $config the configuration of the column at the extendedSummary
  676. *
  677. * @return mixed
  678. * @throws CException
  679. */
  680. protected function getSummaryOperationInstance($name, $config)
  681. {
  682. if (!isset($config['class'])) {
  683. throw new CException(Yii::t(
  684. 'zii',
  685. 'Column summary configuration must be an array containing a "type" element.'
  686. ));
  687. }
  688. if (!in_array($config['class'], $this->extendedSummaryOperations)) {
  689. throw new CException(Yii::t(
  690. 'zii',
  691. '"{operation}" is an unsupported class operation.',
  692. array('{operation}' => $config['class'])
  693. ));
  694. }
  695. // name of the column should be unique
  696. if (!isset($this->extendedSummaryTypes[$name])) {
  697. $this->extendedSummaryTypes[$name] = Yii::createComponent($config);
  698. $this->extendedSummaryTypes[$name]->init();
  699. }
  700. return $this->extendedSummaryTypes[$name];
  701. }
  702. /**
  703. *### .getColumnByName()
  704. *
  705. * Helper function to get a column by its name
  706. *
  707. * @param string $name
  708. *
  709. * @return CDataColumn|null
  710. */
  711. protected function getColumnByName($name)
  712. {
  713. foreach ($this->columns as $column) {
  714. if (strcmp($column->name, $name) === 0) {
  715. return $column;
  716. }
  717. }
  718. return null;
  719. }
  720. }
  721. /**
  722. * TbOperation class
  723. *
  724. * Abstract class where all types of operations extend from
  725. */
  726. abstract class TbOperation extends CWidget
  727. {
  728. /**
  729. * @var string $template the template to display label and value of the operation at the summary
  730. */
  731. public $template = '{label}: {value}';
  732. /**
  733. * @var int $value the resulted value of operation
  734. */
  735. public $value = 0;
  736. /**
  737. * @var string $label the label of the calculated value
  738. */
  739. public $label;
  740. /**
  741. * @var TbDataColumn $column
  742. */
  743. public $column;
  744. /**
  745. * Widget initialization
  746. * @throws CException
  747. */
  748. public function init()
  749. {
  750. if (null == $this->column) {
  751. throw new CException(Yii::t(
  752. 'zii',
  753. '"{attribute}" attribute must be defined',
  754. array('{attribute}' => 'column')
  755. ));
  756. }
  757. }
  758. /**
  759. * Widget's run method
  760. */
  761. public function run()
  762. {
  763. $this->displaySummary();
  764. }
  765. /**
  766. * Process the row data value
  767. *
  768. * @param $value
  769. *
  770. * @return mixed
  771. */
  772. abstract public function processValue($value);
  773. /**
  774. * Displays the resulting summary
  775. * @return mixed
  776. */
  777. abstract public function displaySummary();
  778. }
  779. /**
  780. * TbSumOperation class
  781. *
  782. * Displays a total of specified column name.
  783. *
  784. */
  785. class TbSumOperation extends TbOperation
  786. {
  787. /**
  788. * @var float $total the total sum
  789. */
  790. protected $total;
  791. /**
  792. * @var array $supportedTypes the supported type of values
  793. */
  794. protected $supportedTypes = array('raw', 'text', 'ntext', 'number');
  795. /**
  796. * Widget's initialization method
  797. * @throws CException
  798. */
  799. public function init()
  800. {
  801. parent::init();
  802. if (!in_array($this->column->type, $this->supportedTypes)) {
  803. throw new CException(Yii::t(
  804. 'zii',
  805. 'Unsupported column type. Supported column types are: "{types}"',
  806. array(
  807. '{types}' => implode(', ', $this->supportedTypes)
  808. )
  809. ));
  810. }
  811. }
  812. /**
  813. * Extracts the digital part of the calculated value.
  814. *
  815. * @param int $value
  816. *
  817. * @return bool
  818. */
  819. protected function extractNumber($value)
  820. {
  821. preg_match_all('/([+-]?[0-9]+[,\.]?)+/', $value, $matches);
  822. return !empty($matches[0]) && @$matches[0][0] ? $matches[0][0] : 0;
  823. }
  824. /**
  825. * Process the value to calculate
  826. *
  827. * @param $value
  828. *
  829. * @return mixed|void
  830. */
  831. public function processValue($value)
  832. {
  833. // remove html tags as we cannot access renderDataCellContent from the column
  834. $clean = strip_tags($value);
  835. $this->total += ((float)$this->extractNumber($clean));
  836. }
  837. /**
  838. * Displays the summary
  839. * @return mixed|void
  840. */
  841. public function displaySummary()
  842. {
  843. echo strtr(
  844. $this->template,
  845. array(
  846. '{label}' => $this->label,
  847. '{value}' => $this->total === null ? '' : Yii::app()->format->format($this->total, $this->column->type)
  848. )
  849. );
  850. }
  851. }
  852. /**
  853. * TbCountOfTypeOperation class
  854. *
  855. * Renders a summary based on the count of specified types. For example, if a value has a type 'blue', this class will
  856. * count the number of times the value 'blue' has on that column.
  857. */
  858. class TbCountOfTypeOperation extends TbOperation
  859. {
  860. /**
  861. * @var string $template
  862. * @see parent class
  863. */
  864. public $template = '{label}: {types}';
  865. /**
  866. * @var string $typeTemplate holds the template of each calculated type
  867. */
  868. public $typeTemplate = '{label}({value})';
  869. /**
  870. * @var array $types hold the configuration of types to calculate. The configuration is set by an array which keys
  871. * are the value types to count. You can set their 'label' independently.
  872. *
  873. * <pre>
  874. * 'types' => array(
  875. * '0' => array('label' => 'zeros'),
  876. * '1' => array('label' => 'ones'),
  877. * '2' => array('label' => 'twos')
  878. * </pre>
  879. */
  880. public $types = array();
  881. /**
  882. * Widget's initialization
  883. * @throws CException
  884. */
  885. public function init()
  886. {
  887. if (empty($this->types)) {
  888. throw new CException(Yii::t(
  889. 'zii',
  890. '"{attribute}" attribute must be defined',
  891. array('{attribute}' => 'types')
  892. ));
  893. }
  894. foreach ($this->types as $type) {
  895. if (!isset($type['label'])) {
  896. throw new CException(Yii::t('zii', 'The "label" of a type must be defined.'));
  897. }
  898. }
  899. parent::init();
  900. }
  901. /**
  902. * (no phpDoc)
  903. * @see TbOperation
  904. *
  905. * @param $value
  906. *
  907. * @return mixed|void
  908. */
  909. public function processValue($value)
  910. {
  911. $clean = strip_tags($value);
  912. if (array_key_exists($clean, $this->types)) {
  913. if (!isset($this->types[$clean]['value'])) {
  914. $this->types[$clean]['value'] = 0;
  915. }
  916. $this->types[$clean]['value'] += 1;
  917. }
  918. }
  919. /**
  920. * (no phpDoc)
  921. * @see TbOperation
  922. * @return mixed|void
  923. */
  924. public function displaySummary()
  925. {
  926. $typesResults = array();
  927. foreach ($this->types as $type) {
  928. if (!isset($type['value'])) {
  929. $type['value'] = 0;
  930. }
  931. $typesResults[] = strtr(
  932. $this->typeTemplate,
  933. array('{label}' => $type['label'], '{value}' => $type['value'])
  934. );
  935. }
  936. echo strtr($this->template, array('{label}' => $this->label, '{types}' => implode(' ', $typesResults)));
  937. }
  938. }
  939. /**
  940. * TbPercentOfTypeOperation class
  941. * Renders a summary based on the percent count of specified types. For example, if a value has a type 'blue', this class will
  942. * count the percentage number of times the value 'blue' has on that column.
  943. */
  944. class TbPercentOfTypeOperation extends TbCountOfTypeOperation
  945. {
  946. /**
  947. * @var string $typeTemplate
  948. * @see TbCountOfTypeOperation
  949. */
  950. public $typeTemplate = '{label}({value}%)';
  951. /**
  952. * @var integer $_total holds the total sum of the values. Required to get the percentage.
  953. */
  954. protected $_total;
  955. /**
  956. * @return mixed|void
  957. * @see TbOperation
  958. */
  959. public function displaySummary()
  960. {
  961. $typesResults = array();
  962. foreach ($this->types as $type) {
  963. if (!isset($type['value'])) {
  964. $type['value'] = 0;
  965. }
  966. $type['value'] = $this->getTotal() ? number_format((float)($type['value'] / $this->getTotal()) * 100, 1)
  967. : 0;
  968. $typesResults[] = strtr(
  969. $this->typeTemplate,
  970. array('{label}' => $type['label'], '{value}' => $type['value'])
  971. );
  972. }
  973. echo strtr($this->template, array('{label}' => $this->label, '{types}' => implode(' ', $typesResults)));
  974. }
  975. /**
  976. * Returns the total of types
  977. * @return int holds
  978. */
  979. protected function getTotal()
  980. {
  981. if (null == $this->_total) {
  982. $this->_total = 0;
  983. foreach ($this->types as $type) {
  984. if (isset($type['value'])) {
  985. $this->_total += $type['value'];
  986. }
  987. }
  988. }
  989. return $this->_total;
  990. }
  991. }
  992. /**
  993. * TbPercentOfTypeGooglePieOperation class
  994. *
  995. * Displays a Google visualization pie chart based on the percentage count of type.
  996. */
  997. class TbPercentOfTypeGooglePieOperation extends TbPercentOfTypeOperation
  998. {
  999. /**
  1000. * @var string $chartCssClass the class name of the layer holding the chart
  1001. */
  1002. public $chartCssClass = 'bootstrap-operation-google-pie-chart';
  1003. /**
  1004. * The options
  1005. * @var array $chartOptions
  1006. * @see https://google-developers.appspot.com/chart/interactive/docs/gallery/piechart
  1007. */
  1008. public $chartOptions = array(
  1009. 'title' => 'Google Pie Chart'
  1010. );
  1011. /**
  1012. * @var array $data the configuration data of the chart
  1013. */
  1014. protected $data = array();
  1015. /**
  1016. * @see TbOperation
  1017. * @return mixed|void
  1018. */
  1019. public function displaySummary()
  1020. {
  1021. $this->data[] = array('Label', 'Percent');
  1022. foreach ($this->types as $type) {
  1023. if (!isset($type['value'])) {
  1024. $type['value'] = 0;
  1025. }
  1026. $this->data[] = $this->getTotal() ? array(
  1027. $type['label'],
  1028. (float)number_format(($type['value'] / $this->getTotal()) * 100, 1)
  1029. ) : 0;
  1030. }
  1031. $data = CJavaScript::jsonEncode($this->data);
  1032. $options = CJavaScript::jsonEncode($this->chartOptions);
  1033. echo "<div id='{$this->id}' class='{$this->chartCssClass}' data-data='{$data}' data-options='{$options}'></div>";
  1034. $this->registerClientScript();
  1035. }
  1036. /**
  1037. * Registers required scripts
  1038. */
  1039. public function registerClientScript()
  1040. {
  1041. $chart = Yii::createComponent(
  1042. array(
  1043. 'class' => 'bootstrap.widgets.TbGoogleVisualizationChart',
  1044. 'visualization' => 'PieChart',
  1045. 'containerId' => $this->getId(),
  1046. 'data' => $this->data,
  1047. 'options' => $this->chartOptions
  1048. )
  1049. );
  1050. $chart->init();
  1051. $chart->run();
  1052. /**
  1053. * create custom chart update by using the global chart variable
  1054. * @see TbGoogleVisualizationChart
  1055. */
  1056. $this->column->grid->componentsAfterAjaxUpdate[__CLASS__] =
  1057. 'var $el = $("#' . $this->getId() . '");var data = $el.data("data");var opts = $el.data("options");
  1058. data = google.visualization.arrayToDataTable(data);
  1059. ' . $chart->getId() . '=new google.visualization.PieChart(document.getElementById("' . $this->getId() . '"));
  1060. ' . $chart->getId() . '.draw(data,opts);';
  1061. }
  1062. }
  1063. /**
  1064. * TbPercentOfTypeEasyPieOperation class
  1065. *
  1066. * Displays an chart based on jquery.easy.pie plugin
  1067. */
  1068. class TbPercentOfTypeEasyPieOperation extends TbPercentOfTypeOperation
  1069. {
  1070. /**
  1071. * @var string $chartCssClass the class of the layer containing the class
  1072. */
  1073. public $chartCssClass = 'bootstrap-operation-easy-pie-chart';
  1074. /**
  1075. * @var string $template
  1076. * @see TbOperation
  1077. */
  1078. public $template = '<div style="clear:both">{label}: </div>{types}';
  1079. /**
  1080. * @var string $typeTemplate
  1081. * @see parent class
  1082. */
  1083. public $typeTemplate = '<div style="float:left;text-align:center;margin:2px"><div class="{class}" data-percent="{value}">{value}%</div><div>{label}</div></div>';
  1084. // easy-pie-chart plugin options
  1085. // @see https://github.com/rendro/easy-pie-chart#configuration-parameter
  1086. public $chartOptions = array(
  1087. 'barColor' => '#ef1e25',
  1088. // The color of the curcular bar. You can pass either a css valid color string like rgb,
  1089. // rgba hex or string colors. But you can also pass a function that accepts the current
  1090. // percentage as a value to return a dynamically generated color.
  1091. 'trackColor' => '#f2f2f2',
  1092. // The color of the track for the bar, false to disable rendering.
  1093. 'scaleColor' => '#dfe0e0',
  1094. // The color of the scale lines, false to disable rendering.
  1095. 'lineCap' => 'round',
  1096. // Defines how the ending of the bar line looks like. Possible values are: butt, round and square.
  1097. 'lineWidth' => 5,
  1098. // Width of the bar line in px.
  1099. 'size' => 80,
  1100. // Size of the pie chart in px. It will always be a square.
  1101. 'animate' => false,
  1102. // Time in milliseconds for a eased animation of the bar growing, or false to deactivate.
  1103. 'onStart' => 'js:$.noop',
  1104. // Callback function that is called at the start of any animation (only if animate is not false).
  1105. 'onStop' => 'js:$.noop'
  1106. // Callback function that is called at the end of any animation (only if animate is not false).
  1107. );
  1108. /**
  1109. * @see TbOperation
  1110. * @return mixed|void
  1111. */
  1112. public function displaySummary()
  1113. {
  1114. $this->typeTemplate = strtr($this->typeTemplate, array('{class}' => $this->chartCssClass));
  1115. parent::displaySummary();
  1116. $this->registerClientScripts();
  1117. }
  1118. /**
  1119. * Register required scripts
  1120. */
  1121. protected function registerClientScripts()
  1122. {
  1123. Yii::app()->bootstrap->registerAssetCss('easy-pie-chart.css');
  1124. Yii::app()->bootstrap->registerAssetJs('jquery.easy.pie.chart.js');
  1125. $options = CJavaScript::encode($this->chartOptions);
  1126. Yii::app()->getClientScript()->registerScript(
  1127. __CLASS__ . '#percent-of-type-operation-simple-pie',
  1128. '
  1129. $("#' . $this->column->grid->id . ' .' . $this->column->grid->extendedSummaryCssClass . ' .' . $this->chartCssClass . '")
  1130. .easyPieChart(' . $options . ');
  1131. '
  1132. );
  1133. $this->column->grid->componentsReadyScripts[__CLASS__] =
  1134. $this->column->grid->componentsAfterAjaxUpdate[__CLASS__] =
  1135. '$("#' . $this->column->grid->id . ' .' . $this->column->grid->extendedSummaryCssClass . ' .' . $this->chartCssClass . '")
  1136. .easyPieChart(' . $options . ');';
  1137. }
  1138. }