* @link http://www.yiiframework.com/
* @copyright 2008-2013 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
Yii::import('zii.widgets.CBaseListView');
Yii::import('zii.widgets.grid.CDataColumn');
Yii::import('zii.widgets.grid.CLinkColumn');
Yii::import('zii.widgets.grid.CButtonColumn');
Yii::import('zii.widgets.grid.CCheckBoxColumn');
/**
* CGridView displays a list of data items in terms of a table.
*
* Each row of the table represents the data of a single data item, and a column usually represents
* an attribute of the item (some columns may correspond to complex expression of attributes or static text).
*
* CGridView supports both sorting and pagination of the data items. The sorting
* and pagination can be done in AJAX mode or normal page request. A benefit of using CGridView is that
* when the user browser disables JavaScript, the sorting and pagination automatically degenerate
* to normal page requests and are still functioning as expected.
*
* CGridView should be used together with a {@link IDataProvider data provider}, preferably a
* {@link CActiveDataProvider}.
*
* The minimal code needed to use CGridView is as follows:
*
*
* $dataProvider=new CActiveDataProvider('Post');
*
* $this->widget('zii.widgets.grid.CGridView', array(
* 'dataProvider'=>$dataProvider,
* ));
*
*
* The above code first creates a data provider for the Post
ActiveRecord class.
* It then uses CGridView to display every attribute in every Post
instance.
* The displayed table is equiped with sorting and pagination functionality.
*
* In order to selectively display attributes with different formats, we may configure the
* {@link CGridView::columns} property. For example, we may specify only the title
* and create_time
attributes to be displayed, and the create_time
* should be properly formatted to show as a time. We may also display the attributes of the related
* objects using the dot-syntax as shown below:
*
*
* $this->widget('zii.widgets.grid.CGridView', array(
* 'dataProvider'=>$dataProvider,
* 'columns'=>array(
* 'title', // display the 'title' attribute
* 'category.name', // display the 'name' attribute of the 'category' relation
* 'content:html', // display the 'content' attribute as purified HTML
* array( // display 'create_time' using an expression
* 'name'=>'create_time',
* 'value'=>'date("M j, Y", $data->create_time)',
* ),
* array( // display 'author.username' using an expression
* 'name'=>'authorName',
* 'value'=>'$data->author->username',
* ),
* array( // display a column with "view", "update" and "delete" buttons
* 'class'=>'CButtonColumn',
* ),
* ),
* ));
*
*
* Please refer to {@link columns} for more details about how to configure this property.
*
* @property boolean $hasFooter Whether the table should render a footer.
* This is true if any of the {@link columns} has a true {@link CGridColumn::hasFooter} value.
* @property CFormatter $formatter The formatter instance. Defaults to the 'format' application component.
*
* @author Qiang Xue
* @package zii.widgets.grid
* @since 1.1
*/
class CGridView extends CBaseListView
{
const FILTER_POS_HEADER='header';
const FILTER_POS_FOOTER='footer';
const FILTER_POS_BODY='body';
private $_formatter;
/**
* @var array grid column configuration. Each array element represents the configuration
* for one particular grid column which can be either a string or an array.
*
* When a column is specified as a string, it should be in the format of "name:type:header",
* where "type" and "header" are optional. A {@link CDataColumn} instance will be created in this case,
* whose {@link CDataColumn::name}, {@link CDataColumn::type} and {@link CDataColumn::header}
* properties will be initialized accordingly.
*
* When a column is specified as an array, it will be used to create a grid column instance, where
* the 'class' element specifies the column class name (defaults to {@link CDataColumn} if absent).
* Currently, these official column classes are provided: {@link CDataColumn},
* {@link CLinkColumn}, {@link CButtonColumn} and {@link CCheckBoxColumn}.
*/
public $columns=array();
/**
* @var array the CSS class names for the table body rows. If multiple CSS class names are given,
* they will be assigned to the rows sequentially and repeatedly. This property is ignored
* if {@link rowCssClassExpression} is set. Defaults to array('odd', 'even')
.
* @see rowCssClassExpression
*/
public $rowCssClass=array('odd','even');
/**
* @var string a PHP expression that is evaluated for every table body row and whose result
* is used as the CSS class name for the row. In this expression, you can use the following variables:
*
* $row
the row number (zero-based)
* $data
the data model for the row
* $this
the grid view object
*
* The PHP expression will be evaluated using {@link evaluateExpression}.
*
* A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
* please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
* @see rowCssClass
* @deprecated in 1.1.13 in favor of {@link rowHtmlOptionsExpression}
*/
public $rowCssClassExpression;
/**
* @var string a PHP expression that is evaluated for every table body row and whose result
* is used as additional HTML attributes for the row. The expression should return an
* array whose key value pairs correspond to html attribute and value.
* In this expression, you can use the following variables:
*
* $row
the row number (zero-based)
* $data
the data model for the row
* $this
the grid view object
*
* The PHP expression will be evaluated using {@link evaluateExpression}.
*
* A PHP expression can be any PHP code that has a value. To learn more about what an expression is,
* please refer to the {@link http://www.php.net/manual/en/language.expressions.php php manual}.
* @since 1.1.13
*/
public $rowHtmlOptionsExpression;
/**
* @var boolean whether to display the table even when there is no data. Defaults to true.
* The {@link emptyText} will be displayed to indicate there is no data.
*/
public $showTableOnEmpty=true;
/**
* @var mixed the ID of the container whose content may be updated with an AJAX response.
* Defaults to null, meaning the container for this grid view instance.
* If it is set false, it means sorting and pagination will be performed in normal page requests
* instead of AJAX requests. If the sorting and pagination should trigger the update of multiple
* containers' content in AJAX fashion, these container IDs may be listed here (separated with comma).
*/
public $ajaxUpdate;
/**
* @var string the jQuery selector of the HTML elements that may trigger AJAX updates when they are clicked.
* These tokens are recognized: {page} and {sort}. They will be replaced with the pagination and sorting links selectors.
* Defaults to '{page}, {sort}', that means that the pagination links and the sorting links will trigger AJAX updates.
* Tokens are available from 1.1.11
*
* Note: if this value is empty an exception will be thrown.
*
* Example (adding a custom selector to the default ones):
*
* ...
* 'updateSelector'=>'{page}, {sort}, #mybutton',
* ...
*
* @since 1.1.7
*/
public $updateSelector='{page}, {sort}';
/**
* @var string a javascript function that will be invoked if an AJAX update error occurs.
*
* The function signature is function(xhr, textStatus, errorThrown, errorMessage)
*
* xhr
is the XMLHttpRequest object.
* textStatus
is a string describing the type of error that occurred.
* Possible values (besides null) are "timeout", "error", "notmodified" and "parsererror"
* errorThrown
is an optional exception object, if one occurred.
* errorMessage
is the CGridView default error message derived from xhr and errorThrown.
* Useful if you just want to display this error differently. CGridView by default displays this error with an javascript.alert()
*
* Note: This handler is not called for JSONP requests, because they do not use an XMLHttpRequest.
*
* Example (add in a call to CGridView):
*
* ...
* 'ajaxUpdateError'=>'function(xhr,ts,et,err,id){ $("#"+id).text(err); }',
* ...
*
*/
public $ajaxUpdateError;
/**
* @var string the name of the GET variable that indicates the request is an AJAX request triggered
* by this widget. Defaults to 'ajax'. This is effective only when {@link ajaxUpdate} is not false.
*/
public $ajaxVar='ajax';
/**
* @var mixed the URL for the AJAX requests should be sent to. {@link CHtml::normalizeUrl()} will be
* called on this property. If not set, the current page URL will be used for AJAX requests.
* @since 1.1.8
*/
public $ajaxUrl;
/**
* @var string the type ('GET' or 'POST') of the AJAX requests. If not set, 'GET' will be used.
* You can set this to 'POST' if you are filtering by many fields at once and have a problem with GET query string length.
* Note that in POST mode direct links and {@link enableHistory} feature may not work correctly!
* @since 1.1.14
*/
public $ajaxType;
/**
* @var string a javascript function that will be invoked before an AJAX update occurs.
* The function signature is function(id,options)
where 'id' refers to the ID of the grid view,
* 'options' the AJAX request options (see jQuery.ajax api manual).
*/
public $beforeAjaxUpdate;
/**
* @var string a javascript function that will be invoked after a successful AJAX response is received.
* The function signature is function(id, data)
where 'id' refers to the ID of the grid view,
* 'data' the received ajax response data.
*/
public $afterAjaxUpdate;
/**
* @var string a javascript function that will be invoked after the row selection is changed.
* The function signature is function(id)
where 'id' refers to the ID of the grid view.
* In this function, you may use $(gridID).yiiGridView('getSelection')
to get the key values
* of the currently selected rows (gridID is the DOM selector of the grid).
* @see selectableRows
*/
public $selectionChanged;
/**
* @var integer the number of table body rows that can be selected. If 0, it means rows cannot be selected.
* If 1, only one row can be selected. If 2 or any other number, it means multiple rows can be selected.
* A selected row will have a CSS class named 'selected'. You may also call the JavaScript function
* $(gridID).yiiGridView('getSelection')
to retrieve the key values of the currently selected
* rows (gridID is the DOM selector of the grid).
*/
public $selectableRows=1;
/**
* @var string the base script URL for all grid view resources (eg javascript, CSS file, images).
* Defaults to null, meaning using the integrated grid view resources (which are published as assets).
*/
public $baseScriptUrl;
/**
* @var string the URL of the CSS file used by this grid view. Defaults to null, meaning using the integrated
* CSS file. If this is set false, you are responsible to explicitly include the necessary CSS file in your page.
*/
public $cssFile;
/**
* @var string the text to be displayed in a data cell when a data value is null. This property will NOT be HTML-encoded
* when rendering. Defaults to an HTML blank.
*/
public $nullDisplay=' ';
/**
* @var string the text to be displayed in an empty grid cell. This property will NOT be HTML-encoded when rendering. Defaults to an HTML blank.
* This differs from {@link nullDisplay} in that {@link nullDisplay} is only used by {@link CDataColumn} to render
* null data values.
* @since 1.1.7
*/
public $blankDisplay=' ';
/**
* @var string the CSS class name that will be assigned to the widget container element
* when the widget is updating its content via AJAX. Defaults to 'grid-view-loading'.
* @since 1.1.1
*/
public $loadingCssClass='grid-view-loading';
/**
* @var string the jQuery selector of filter input fields.
* The token '{filter}' is recognized and it will be replaced with the grid filters selector.
* Defaults to '{filter}'.
*
* Note: if this value is empty an exception will be thrown.
*
* Example (adding a custom selector to the default one):
*
* ...
* 'filterSelector'=>'{filter}, #myfilter',
* ...
*
* @since 1.1.13
*/
public $filterSelector='{filter}';
/**
* @var string the CSS class name for the table row element containing all filter input fields. Defaults to 'filters'.
* @see filter
* @since 1.1.1
*/
public $filterCssClass='filters';
/**
* @var string whether the filters should be displayed in the grid view. Valid values include:
*
* - header: the filters will be displayed on top of each column's header cell.
* - body: the filters will be displayed right below each column's header cell.
* - footer: the filters will be displayed below each column's footer cell.
*
* @see filter
* @since 1.1.1
*/
public $filterPosition='body';
/**
* @var CModel the model instance that keeps the user-entered filter data. When this property is set,
* the grid view will enable column-based filtering. Each data column by default will display a text field
* at the top that users can fill in to filter the data.
* Note that in order to show an input field for filtering, a column must have its {@link CDataColumn::name}
* property set or have {@link CDataColumn::filter} as the HTML code for the input field.
* When this property is not set (null) the filtering is disabled.
* @since 1.1.1
*/
public $filter;
/**
* @var boolean whether to hide the header cells of the grid. When this is true, header cells
* will not be rendered, which means the grid cannot be sorted anymore since the sort links are located
* in the header. Defaults to false.
* @since 1.1.1
*/
public $hideHeader=false;
/**
* @var boolean whether to leverage the {@link https://developer.mozilla.org/en/DOM/window.history DOM history object}. Set this property to true
* to persist state of grid across page revisits. Note, there are two limitations for this feature:
*
* - this feature is only compatible with browsers that support HTML5.
* - expect unexpected functionality (e.g. multiple ajax calls) if there is more than one grid/list on a single page with enableHistory turned on.
*
* @since 1.1.11
*/
public $enableHistory=false;
/**
* Initializes the grid view.
* This method will initialize required property values and instantiate {@link columns} objects.
*/
public function init()
{
parent::init();
if(empty($this->updateSelector))
throw new CException(Yii::t('zii','The property updateSelector should be defined.'));
if(empty($this->filterSelector))
throw new CException(Yii::t('zii','The property filterSelector should be defined.'));
if(!isset($this->htmlOptions['class']))
$this->htmlOptions['class']='grid-view';
if($this->baseScriptUrl===null)
$this->baseScriptUrl=Yii::app()->getAssetManager()->publish(Yii::getPathOfAlias('zii.widgets.assets')).'/gridview';
if($this->cssFile!==false)
{
if($this->cssFile===null)
$this->cssFile=$this->baseScriptUrl.'/styles.css';
Yii::app()->getClientScript()->registerCssFile($this->cssFile);
}
$this->initColumns();
}
/**
* Creates column objects and initializes them.
*/
protected function initColumns()
{
if($this->columns===array())
{
if($this->dataProvider instanceof CActiveDataProvider)
$this->columns=$this->dataProvider->model->attributeNames();
elseif($this->dataProvider instanceof IDataProvider)
{
// use the keys of the first row of data as the default columns
$data=$this->dataProvider->getData();
if(isset($data[0]) && is_array($data[0]))
$this->columns=array_keys($data[0]);
}
}
$id=$this->getId();
foreach($this->columns as $i=>$column)
{
if(is_string($column))
$column=$this->createDataColumn($column);
else
{
if(!isset($column['class']))
$column['class']='CDataColumn';
$column=Yii::createComponent($column, $this);
}
if(!$column->visible)
{
unset($this->columns[$i]);
continue;
}
if($column->id===null)
$column->id=$id.'_c'.$i;
$this->columns[$i]=$column;
}
foreach($this->columns as $column)
$column->init();
}
/**
* Creates a {@link CDataColumn} based on a shortcut column specification string.
* @param string $text the column specification string
* @return CDataColumn the column instance
*/
protected function createDataColumn($text)
{
if(!preg_match('/^([\w\.]+)(:(\w*))?(:(.*))?$/',$text,$matches))
throw new CException(Yii::t('zii','The column must be specified in the format of "Name:Type:Label", where "Type" and "Label" are optional.'));
$column=new CDataColumn($this);
$column->name=$matches[1];
if(isset($matches[3]) && $matches[3]!=='')
$column->type=$matches[3];
if(isset($matches[5]))
$column->header=$matches[5];
return $column;
}
/**
* Registers necessary client scripts.
*/
public function registerClientScript()
{
$id=$this->getId();
if($this->ajaxUpdate===false)
$ajaxUpdate=false;
else
$ajaxUpdate=array_unique(preg_split('/\s*,\s*/',$this->ajaxUpdate.','.$id,-1,PREG_SPLIT_NO_EMPTY));
$options=array(
'ajaxUpdate'=>$ajaxUpdate,
'ajaxVar'=>$this->ajaxVar,
'pagerClass'=>$this->pagerCssClass,
'loadingClass'=>$this->loadingCssClass,
'filterClass'=>$this->filterCssClass,
'tableClass'=>$this->itemsCssClass,
'selectableRows'=>$this->selectableRows,
'enableHistory'=>$this->enableHistory,
'updateSelector'=>$this->updateSelector,
'filterSelector'=>$this->filterSelector
);
if($this->ajaxUrl!==null)
$options['url']=CHtml::normalizeUrl($this->ajaxUrl);
if($this->ajaxType!==null) {
$options['ajaxType']=strtoupper($this->ajaxType);
$request=Yii::app()->getRequest();
if ($options['ajaxType']=='POST' && $request->enableCsrfValidation) {
$options['csrfTokenName']=$request->csrfTokenName;
$options['csrfToken']=$request->getCsrfToken();
}
}
if($this->enablePagination)
$options['pageVar']=$this->dataProvider->getPagination()->pageVar;
foreach(array('beforeAjaxUpdate', 'afterAjaxUpdate', 'ajaxUpdateError', 'selectionChanged') as $event)
{
if($this->$event!==null)
{
if($this->$event instanceof CJavaScriptExpression)
$options[$event]=$this->$event;
else
$options[$event]=new CJavaScriptExpression($this->$event);
}
}
$options=CJavaScript::encode($options);
$cs=Yii::app()->getClientScript();
$cs->registerCoreScript('jquery');
$cs->registerCoreScript('bbq');
if($this->enableHistory)
$cs->registerCoreScript('history');
$cs->registerScriptFile($this->baseScriptUrl.'/jquery.yiigridview.js',CClientScript::POS_END);
$cs->registerScript(__CLASS__.'#'.$id,"jQuery('#$id').yiiGridView($options);");
}
/**
* Renders the data items for the grid view.
*/
public function renderItems()
{
if($this->dataProvider->getItemCount()>0 || $this->showTableOnEmpty)
{
echo "itemsCssClass}\">\n";
$this->renderTableHeader();
ob_start();
$this->renderTableBody();
$body=ob_get_clean();
$this->renderTableFooter();
echo $body; // TFOOT must appear before TBODY according to the standard.
echo "
";
}
else
$this->renderEmptyText();
}
/**
* Renders the table header.
*/
public function renderTableHeader()
{
if(!$this->hideHeader)
{
echo "\n";
if($this->filterPosition===self::FILTER_POS_HEADER)
$this->renderFilter();
echo "\n";
foreach($this->columns as $column)
$column->renderHeaderCell();
echo "
\n";
if($this->filterPosition===self::FILTER_POS_BODY)
$this->renderFilter();
echo "\n";
}
elseif($this->filter!==null && ($this->filterPosition===self::FILTER_POS_HEADER || $this->filterPosition===self::FILTER_POS_BODY))
{
echo "\n";
$this->renderFilter();
echo "\n";
}
}
/**
* Renders the filter.
* @since 1.1.1
*/
public function renderFilter()
{
if($this->filter!==null)
{
echo "filterCssClass}\">\n";
foreach($this->columns as $column)
$column->renderFilterCell();
echo "
\n";
}
}
/**
* Renders the table footer.
*/
public function renderTableFooter()
{
$hasFilter=$this->filter!==null && $this->filterPosition===self::FILTER_POS_FOOTER;
$hasFooter=$this->getHasFooter();
if($hasFilter || $hasFooter)
{
echo "\n";
if($hasFooter)
{
echo "\n";
foreach($this->columns as $column)
$column->renderFooterCell();
echo "
\n";
}
if($hasFilter)
$this->renderFilter();
echo "\n";
}
}
/**
* Renders the table body.
*/
public function renderTableBody()
{
$data=$this->dataProvider->getData();
$n=count($data);
echo "\n";
if($n>0)
{
for($row=0;$row<$n;++$row)
$this->renderTableRow($row);
}
else
{
echo '';
$this->renderEmptyText();
echo " |
\n";
}
echo "\n";
}
/**
* Renders a table body row.
* @param integer $row the row number (zero-based).
*/
public function renderTableRow($row)
{
$htmlOptions=array();
if($this->rowHtmlOptionsExpression!==null)
{
$data=$this->dataProvider->data[$row];
$options=$this->evaluateExpression($this->rowHtmlOptionsExpression,array('row'=>$row,'data'=>$data));
if(is_array($options))
$htmlOptions = $options;
}
if($this->rowCssClassExpression!==null)
{
$data=$this->dataProvider->data[$row];
$class=$this->evaluateExpression($this->rowCssClassExpression,array('row'=>$row,'data'=>$data));
}
elseif(is_array($this->rowCssClass) && ($n=count($this->rowCssClass))>0)
$class=$this->rowCssClass[$row%$n];
if(!empty($class))
{
if(isset($htmlOptions['class']))
$htmlOptions['class'].=' '.$class;
else
$htmlOptions['class']=$class;
}
echo CHtml::openTag('tr', $htmlOptions)."\n";
foreach($this->columns as $column)
$this->renderDataCell($column, $row);
echo "\n";
}
/**
* A seam for people extending CGridView to be able to hook onto the data cell rendering process.
*
* By overriding only this method we will not need to copypaste and modify the whole entirety of `renderTableRow`.
* Or override `renderDataCell()` method of all possible CGridColumn descendants.
*
* @param CGridColumn $column The Column instance to
* @param integer $row
* @since 1.1.17
*/
protected function renderDataCell($column, $row)
{
$column->renderDataCell($row);
}
/**
* @return boolean whether the table should render a footer.
* This is true if any of the {@link columns} has a true {@link CGridColumn::hasFooter} value.
*/
public function getHasFooter()
{
foreach($this->columns as $column)
if($column->getHasFooter())
return true;
return false;
}
/**
* @return CFormatter the formatter instance. Defaults to the 'format' application component.
*/
public function getFormatter()
{
if($this->_formatter===null)
$this->_formatter=Yii::app()->format;
return $this->_formatter;
}
/**
* @param CFormatter $value the formatter instance
*/
public function setFormatter($value)
{
$this->_formatter=$value;
}
}