Incam_IntranetNueva/www/protected/extensions/multimodelform/MultiModelForm.php

1161 lines
37 KiB
PHP
Raw Normal View History

<?php
/**
* MultiModelForm.php
*
* Handling of multiple records and models in a form
*
* Uses the jQuery plugin RelCopy
* @link http://www.andresvidal.com/labs/relcopy.html
*
* @author Joe Blocher <yii@myticket.at>
* @copyright 2011 myticket it-solutions gmbh
* @license New BSD License
* @category User Interface
* @version 3.0
*/
class MultiModelForm extends CWidget
{
const CLASSPREFIX = 'mmf_'; //prefix for tag classes
/**
* The model to handle
*
* @var CModel $model
*/
public $model;
/**
* Configuration of the form provided by the models method getMultiModelForm()
*
* This configuration array defines generation CForm
* Can be a config array or a config file that returns the configuration
*
* @link http://www.yiiframework.com/doc/guide/1.1/en/form.builder
* @var mixed $elements
*/
public $formConfig = array();
/**
* Array of models loaded from db.
* Created for example by $model->findAll();
*
* @var CModel $data
*/
public $data;
/**
* The controller returns all validated items (array of model)
* if a validation error occurs.
* The form will then be rendered with error output.
* $data will be ignored in this case.
* @see method run()
*
* @var array $validatedItems
*/
public $validatedItems;
/**
* Set to true if the error summary should be rendered for the model of this form
*
* @var boolean $showErrorSummary
*/
public $showErrorSummary = false;
/**
* The text of the copy/clone link
*
* @var string $addItemText
*/
public $addItemText = 'Add item';
/**
* Show 'Add item' link and empty item in errormode
*
* @var boolean $allowAddOnError
*/
public $showAddItemOnError = true;
/**
* The text for the remove link
* Can be an image tag too.
* Leave empty to disable removing.
*
* @var string $removeText
*/
public $removeText = 'Remove';
/**
* The confirmation text before remove an item
* Set to null/empty to disable confirmation
*
* @var string $removeText
*/
public $removeConfirm = 'Delete this item?';
/**
* The htmlOptions for the remove link
*
* @var array $removeHtmlOptions
*/
public $removeHtmlOptions = array();
/**
* Show elements as table
* If set to true, $fieldsetWrapper, $rowWrapper and $removeLinkWrapper will be ignored
*
* @var boolean
*/
public $tableView = false;
/**
* The htmlOptions for the table tag
*
* @var array $tableHtmlOptions
*/
public $tableHtmlOptions = array();
/**
* Items are rendered as <tfoot><tr><td>Item1</td><td>Item2</td> ...</tr></tfoot>
*
* @var string $tableFootCells
*/
public $tableFootCells = array();
/**
* Set this attribute to enable manual sorting by drag/drop of the multiple items
* Uses the CJuiSortable widget
*
* @var string the name of the attribute
*/
public $sortAttribute;
/**
* The options property of the zii.widgets.jui.CJuiSortable
*
* @link http://www.yiiframework.com/doc/api/1.1/CJuiWidget#options-detail
* @link http://jqueryui.com/demos/sortable/
*
* @var array
*/
public $sortOptions = array(
'placeholder' => 'ui-state-highlight',
'opacity' => 0.8,
'cursor' => 'move'
);
/**
* The wrapper for each fieldset
*
* @var array $fieldsetWrapper
*/
public $fieldsetWrapper = array(
'tag' => 'div',
'htmlOptions' => array('class' => 'view'), //'fieldset' is unknown in the default css context of form.css
);
/**
* The wrapper for a row
*
* @var array $rowWrapper
*/
public $rowWrapper = array(
'tag' => 'div',
'htmlOptions' => array('class' => 'row'),
);
/**
* The wrapper for the removeLink
*
* @var array $fieldsetWrapper
*/
public $removeLinkWrapper = array(
'tag' => 'span',
'htmlOptions' => array(),
);
/**
* The javascript code jsBeforeClone,jsAfterClone ...
* This allows to handle widgets on cloning.
* Important: 'this' is the current handled jQuery object
* For CJuiDatePicker and extension 'datetimepicker' see prepared php-code below: afterNewIdDatePicker,afterNewIdDateTimePicker
*
* Usage if you have CJuiDatePicker to clone (assume your form elements are defined in the array $formConfig):
* 'jsAfterNewId' => MultiModelForm::afterNewIdDateTimePicker($formConfig['elements']['mydatefield']),
*
*/
public $jsBeforeClone; // 'jsBeforeClone' => "alert(this.attr('class'));";
public $jsAfterClone; // 'jsAfterClone' => "alert(this.attr('class'));";
public $jsBeforeNewId; // 'jsBeforeNewId' => "alert(this.attr('id'));";
public $jsAfterNewId; // 'jsAfterNewId' => "alert(this.attr('id'));";
/**
* Available options for the jQuery plugin RelCopy
*
* string excludeSelector - A jQuery selector used to exclude an element and its children
* integer limit - The number of allowed copies. Default: 0 is unlimited
* string append - Additional HTML to attach at the end of each copy.
* string copyClass - A class to attach to each copy
* boolean clearInputs - Option to clear each copies text input fields or textarea
*
* @link http://www.andresvidal.com/labs/relcopy.html
*
* @var array $options
*/
public $options = array();
/**
* The assets url
*
* @var string $_assets
*/
private $_assets;
/**
* Support for CJuiDatePicker
* Set 'jsAfterNewId'=MultiModelForm::afterNewIdDateTimePicker($myFormConfig['elements']['mydate'])
* if you use at least one datepicker.
*
* The options will be assigned from the config array of the element
*
* @param array $element
* @return string
*/
public static function afterNewIdDatePicker($element)
{
$options = isset($element['options']) ? $element['options'] : array();
$jsOptions = CJavaScript::encode($options);
$language = isset($element['language']) ? $element['language'] : '';
if (!empty($language))
$language = "jQuery.datepicker.regional['$language'],";
return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker'); this.datepicker(jQuery.extend({showMonthAfterYear:false}, $language {$jsOptions}));};";
}
/**
* Support for extension datetimepicker
* @link http://www.yiiframework.com/extension/datetimepicker/
*
* @param array $element
* @return string
*/
public static function afterNewIdDateTimePicker($element)
{
$options = isset($element['options']) ? $element['options'] : array();
$jsOptions = CJavaScript::encode($options);
$language = isset($element['language']) ? $element['language'] : '';
if (!empty($language))
$language = "jQuery.datepicker.regional['$language'],";
return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker').datetimepicker(jQuery.extend($language {$jsOptions}));};";
}
/**
* Support for CJuiAutoComplete: not working - needs review
*
* @param array $element
* @return string
*/
public static function afterNewIdAutoComplete($element)
{
$options = isset($element['options']) ? $element['options'] : array();
if (isset($element['sourceUrl']))
$options['source'] = CHtml::normalizeUrl($element['sourceUrl']);
else
$options['source'] = $element['source'];
$jsOptions = CJavaScript::encode($options);
//return "this.autocomplete($jsOptions);"; //works for non-autocomplete elements
//return "if(this.hasClass('ui-autocomplete-input')) this.autocomplete($jsOptions);";
//return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete($jsOptions);";
//return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete('destroy').autocomplete($jsOptions);";
//return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);";
//return "if(this.hasClass('ui-autocomplete-input')) this.unbind().removeClass('ui-autocomplete-input').removeAttr('autocomplete').removeAttr('role').removeAttr('aria-autocomplete').removeAttr('aria-haspopup').autocomplete($jsOptions);";
}
/**
* This static function should be used in the controllers update action
* The models will be validated before saving
*
* If a record is not valid, the invalid model will be set to $model
* to display error summary
*
* @param mixed $model CActiveRecord or other CModel
* @param array $validatedItems returns the array of validated records
* @param array $deleteItems
* @param array $masterValues attributes to assign before saving
* @param array $formData (default = $_POST)
* @return boolean
*/
public static function save($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null)
{
//validate if empty: means no validation has been done
$doValidate = empty($validatedItems) && empty($deleteItems);
if (!isset($formData))
$formData = $_POST;
$sortAttribute = !empty($formData[self::CLASSPREFIX . 'sortAttribute']) ? $formData[self::CLASSPREFIX . 'sortAttribute'] : null;
$sortIndex = 0;
if ($doValidate)
{
//validate and assign $masterValues
if (!self::validate($model, $validatedItems, $deleteItems, $masterValues, $formData))
return false;
}
if (!empty($validatedItems))
foreach ($validatedItems as $item)
{
if (!$doValidate) //assign $masterValues
{
if (!empty($masterValues))
$item->setAttributes($masterValues, false);
}
//if sortable, assign the sortAttribute
if (!empty($sortAttribute))
{
$sortIndex++;
$item->$sortAttribute = $sortIndex;
}
if (!$item->save())
return false;
}
//$deleteItems = array of primary keys to delete
if (!empty($deleteItems))
foreach ($deleteItems as $pk)
if (!empty($pk))
{
//array doesn't work with activerecord?
if (count($pk == 1))
{
$vals = array_values($pk);
$pk = $vals[0];
}
$model->deleteByPk($pk);
}
return true;
}
/**
* Validates submitted formdata
* If a record is not valid, the invalid model will be set to $model
* to display error summary
*
* @param mixed $model
* @param array $validatedItems returns the array of validated records
* @param array $deleteItems returns the array of model for deleting
* @param array $masterValues attributes to assign before saving
* @param array $formData (default = $_POST)
* @return boolean
*/
public static function validate($model, &$validatedItems, &$deleteItems = array(), $masterValues = array(), $formData = null)
{
$widget = new MultiModelForm;
$widget->model = $model;
$widget->checkModel();
if (!$widget->initItems($validatedItems, $deleteItems, $masterValues, $formData))
return false; //at least one item is not valid
else
return true;
}
/**
* Converts the submitted formdata into an array of model
*
* @param array $formData the postdata $_POST submitted by the form
* @param array $validatedItems the items which were validated
* @param array $deleteItems the items to delete
* @param array $masterValues assign additional masterdata before save
* @return array array of model
*/
public function initItems(&$validatedItems, &$deleteItems, $masterValues = array(), $formData = null)
{
if (!isset($formData))
$formData = $_POST;
$result = true;
$newItems = array();
$validatedItems = array(); //bugfix: 1.0.2
$deleteItems = array();
$modelClass = get_class($this->model);
if (!isset($formData) || empty($formData[$modelClass]))
return true;
//----------- NEW (on validation error) -----------
if (isset($formData[$modelClass]['n__']))
{
foreach ($formData[$modelClass]['n__'] as $idx => $attributes)
{
$model = new $modelClass;
$model->attributes = $attributes;
if (!empty($masterValues))
$model->setAttributes($masterValues, false); //assign mastervalues
// validate
if (!$model->validate())
$result = false;
$validatedItems[] = $model;
}
unset($formData[$modelClass]['n__']);
}
//----------- UPDATE -----------
$allExistingPk = isset($formData[$modelClass]['pk__']) ? $formData[$modelClass]['pk__'] : null; //bugfix: 1.0.1
if (isset($formData[$modelClass]['u__']))
{
foreach ($formData[$modelClass]['u__'] as $idx => $attributes)
{
$model = new $modelClass('update');
//should work for CModel, mongodb models... too
if (method_exists($model, 'setIsNewRecord'))
$model->setIsNewRecord(false);
$model->attributes = $attributes;
if (!empty($masterValues))
$model->setAttributes($masterValues, false); //assign mastervalues
//ensure to assign primary keys (when pk is unsafe or not defined in rules)
if (is_array($allExistingPk))
{
$primaryKeys = $allExistingPk[$idx];
$model->setAttributes($primaryKeys, false);
}
// validate
if (!$model->validate())
$result = false;
$validatedItems[] = $model;
// remove from $allExistingPk
if (is_array($allExistingPk))
unset($allExistingPk[$idx]);
}
unset($formData[$modelClass]['u__']);
}
//----------- DELETE -----------
// add remaining primarykeys to $deleteItems (reindex)
if (is_array($allExistingPk))
foreach ($allExistingPk as $idx => $delPks)
$deleteItems[] = $delPks;
// remove handled formdata pk__
unset($formData[$modelClass]['pk__']);
//----------- Check for cloned elements by jQuery -----------
if(!empty($formData[$modelClass])) //has cloned elements
{
// use the first item as reference
$refAttribute = key($formData[$modelClass]);
$refArray = array_shift($formData[$modelClass]);
if (!empty($refArray))
foreach ($refArray as $idx => $value)
{
// check continue if all values are empty
if (empty($value))
{
$allEmpty = true;
foreach ($formData[$modelClass] as $attrKey => $values)
{
if (is_array($values[$idx])) //bugfix v2.1.1 have to check empty array items too
{
$isEmpty = true;
foreach ($formData[$modelClass][$attrKey] as $item)
{
if (!empty($item[$idx]))
{
$isEmpty = false;
break;
}
}
}
else
$isEmpty = empty($values[$idx]);
$allEmpty = $isEmpty && $allEmpty;
if (!$allEmpty)
break;
}
if ($allEmpty)
continue;
}
$model = new $modelClass;
$model->$refAttribute = $value;
foreach ($formData[$modelClass] as $attrKey => $values)
{
//v2.2 support for checkboxlist / radiolist
if (is_array($values[$idx]))
{
$arrayAttribute = array();
foreach ($formData[$modelClass][$attrKey] as $item)
{
if (!empty($item[$idx]))
$arrayAttribute[] = $item[$idx];
}
$model->$attrKey = $arrayAttribute;
}
else
$model->$attrKey = $values[$idx];
}
//assign mastervalues without checking rules for new records
$model->setAttributes($masterValues, false);
// validate
if (!$model->validate())
$result = false;
$validatedItems[] = $model;
}
}
return $result;
}
/**
* Get the primary key as array (key => value)
*
* @param CModel $model
* @return array
*/
public function getPrimaryKey($model)
{
$result = array();
if ($model instanceof CActiveRecord)
{
$pkValue = $model->primaryKey;
if (!empty($pkValue))
{
$pkName = $model->primaryKey();
if (empty($pkName))
$pkName = $model->tableSchema->primaryKey;
$result = array($pkName => $pkValue);
}
}
else // when working with EMongoDocument
if (method_exists($model, 'primaryKey'))
{
$pkName = $model->primaryKey();
$pkValue = $model->$pkName;
if (empty($pkValue))
$result = array($pkName => $pkValue);
}
return $result;
}
/**
* Get the copyClass
*
* @return string
*/
public function getCopyClass()
{
if (isset($this->options['copyClass']))
return $this->options['copyClass'];
else
{
$selector = $this->id . '_copy';
$this->options['copyClass'] = $selector;
return $selector;
}
}
/**
* The link for removing a fieldset
*
* @return string
*/
public function getRemoveLink()
{
if (empty($this->removeText))
return '';
$onClick = 'jQuery(this).parent().parent().remove(); return false;';
if (!empty($this->removeConfirm))
$onClick = "if(confirm('{$this->removeConfirm}')) " . $onClick;
$htmlOptions = array_merge($this->removeHtmlOptions, array('onclick' => $onClick));
$link = CHtml::link($this->removeText, '#', $htmlOptions);
return CHtml::tag($this->removeLinkWrapper['tag'],
$this->removeLinkWrapper['htmlOptions'], $link);
}
/**
* Check if rows has to be sortable
* Works only if not is as tableView because the submitted $_POST data are not in the correct sorted order
* Sorting in tableView needs more investigation/workaround ...
*
* @return bool
*/
public function isSortable()
{
return !empty($this->sortAttribute) && !$this->tableView;
}
/**
* Initialize the widget: register scripts
*/
public function init()
{
if ($this->tableView)
{
$this->fieldsetWrapper = array('tag' => 'tr', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'row'));
$this->rowWrapper = array('tag' => 'td', 'htmlOptions' => array('class' => self::CLASSPREFIX . 'cell'));
$this->removeLinkWrapper = $this->rowWrapper;
}
$this->checkModel();
$this->registerClientScript();
parent::init();
}
/**
* Check the model instance on init / after create
* Add all model attributes as hidden and visible=false if they are not part of the formConfig
* Need this because on update all attributes have to be submitted, no 'loadModel' is called
*/
protected function checkModel()
{
if (is_string($this->model))
$this->model = new $this->model;
if (isset($this->model) && isset($this->formConfig))
{
// add undefined attributes in the form config as hidden fields and attribute visible = false
if (isset($this->formConfig) && !empty($this->formConfig['elements']))
foreach ($this->model->attributes as $attribute => $value)
{
if (!array_key_exists($attribute, $this->formConfig['elements']))
$this->formConfig['elements'][$attribute] = array('type' => 'hidden', 'visible' => false);
}
}
}
/**
* @return array the javascript options
*/
protected function getClientOptions()
{
if (empty($this->options))
$this->options = array();
if (!empty($this->removeText))
{
$append = $this->getRemoveLink();
$this->options['append'] = empty($this->options['append']) ? $append : $append . ' ' . $this->options['append'];
}
if (!empty($this->jsBeforeClone))
$this->options['beforeClone'] = $this->jsBeforeClone;
if (!empty($this->jsAfterClone))
$this->options['afterClone'] = $this->jsAfterClone;
if (!empty($this->jsBeforeNewId))
$this->options['beforeNewId'] = $this->jsBeforeNewId;
if (!empty($this->jsAfterNewId))
$this->options['afterNewId'] = $this->jsAfterNewId;
return CJavaScript::encode($this->options);
}
/**
* The id selector for jQuery.sortable
*
* @return string
*/
protected function getSortSelectorId()
{
return self::CLASSPREFIX . 'sortable';
}
/**
* Registers the relcopy javascript file.
*/
public function registerClientScript()
{
$cs = Yii::app()->getClientScript();
$this->_assets = Yii::app()->assetManager->publish(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets');
$cs->registerCoreScript('jquery');
$cs->registerScriptFile($this->_assets . '/js/jquery.relcopy.yii.1.0.js');
$options = $this->getClientOptions();
$cs->registerScript(__CLASS__ . '#' . $this->id, "jQuery('#{$this->id}').relCopy($options);");
//add the script for jQuery.sortable
if ($this->isSortable())
{
$cs->registerCoreScript('jquery.ui');
$cssFile = $cs->getCoreScriptUrl() . '/jui/css/base/jquery-ui.css';
$cs->registerCssFile($cssFile);
$options = CJavaScript::encode($this->sortOptions);
Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id . 'Sortable', "jQuery('#{$this->getSortSelectorId()}').sortable({$options});");
}
}
/**
* Render the top of the table: AddLink, Table header
*/
public function renderTableBegin($renderAddLink)
{
$form = new MultiModelRenderForm($this->formConfig, $this->model);
$form->parentWidget = $this;
//add link as div
if ($renderAddLink)
{
$addLink = $form->getAddLink();
echo CHtml::tag('div', array('class' => self::CLASSPREFIX . 'addlink'), $addLink);
}
$tableHtmlOptions = array_merge(array('class' => self::CLASSPREFIX . 'table'), $this->tableHtmlOptions);
//table
echo CHtml::tag('table', $tableHtmlOptions, false, false);
//thead
$form->renderTableHeader();
//tfoot
if (!empty($this->tableFootCells))
{
$cells = '';
foreach ($this->tableFootCells as $cell)
{
$cells .= CHtml::tag('td', array('class' => self::CLASSPREFIX . 'cell'), $cell);
}
$cells = CHtml::tag('tr', array('class' => self::CLASSPREFIX . 'row'), $cells);
echo CHtml::tag('tfoot', array(), $cells);
}
//tbody
$tbodyOptions = $this->isSortable() ? array('id' => $this->getSortSelectorId()) : array();
echo CHtml::tag('tbody', $tbodyOptions, false, false);
}
/**
* Renders the active form if a model and formConfig is set
* $this->data is array of model
*/
public function run()
{
if (empty($this->model) || empty($this->formConfig))
return;
//form is displayed again with some invalid models
$isErrorMode = !empty($this->validatedItems);
$showAddLink = !$isErrorMode || ($isErrorMode && $this->showAddItemOnError);
$this->formConfig['activeForm'] = array('class' => 'MultiModelEmbeddedForm');
$idx = 0;
$errorPk = null;
if ($isErrorMode)
{
if ($this->showErrorSummary)
echo CHtml::errorSummary($this->validatedItems);
$data = $this->validatedItems;
}
else
$data = $this->data; //from the db
if ($this->tableView)
$this->renderTableBegin(false); //RODAX $showAddLink);
if ($this->isSortable())
{
//render the name of the sortAttribute as hidden input
//used in MultiModelForm::save
echo CHtml::hiddenField(self::CLASSPREFIX . 'sortAttribute', $this->sortAttribute);
if (!$this->tableView)
echo CHtml::openTag('div', array('id' => $this->getSortSelectorId()));
}
// existing records
if (is_array($data) && !empty($data))
{
foreach ($data as $model)
{
$form = new MultiModelRenderForm($this->formConfig, $model);
$form->index = $idx;
$form->parentWidget = $this;
$form->primaryKey = $this->getPrimaryKey($model);
if (!$this->tableView)
{
if ($showAddLink && $idx == 0) // no existing data rendered
echo $form->renderAddLink();
}
// render pk outside of removeable tag, for checking records to delete
// see method initItems()
echo $form->renderHiddenPk('[pk__]');
echo $form->render();
$idx++;
}
}
//if form is displayed first time or in errormode and want to show 'Add item' (and a 'CopyTemplate')
if($showAddLink)
{
// add an empty fieldset as CopyTemplate
$form = new MultiModelRenderForm($this->formConfig, $this->model);
$form->index = $idx;
$form->parentWidget = $this;
$form->isCopyTemplate = true;
if (!$this->tableView)
{
if ($idx == 0) // no existing data rendered
echo $form->renderAddLink();
}
echo $form->render();
}
if ($this->tableView)
{
echo CHtml::closeTag('tbody');
echo CHtml::closeTag('table');
}
elseif ($this->isSortable())
echo CHtml::closeTag('div');
}
}
/**
* The CForm to render the input form
*/
class MultiModelRenderForm extends CForm
{
public $parentWidget;
public $index;
public $isCopyTemplate;
public $primaryKey;
/**
* Wraps a content with row wrapper
*
* @param string $content
* @return string
*/
protected function getWrappedRow($content)
{
return CHtml::tag($this->parentWidget->rowWrapper['tag'],
$this->parentWidget->rowWrapper['htmlOptions'], $content);
}
/**
* Wraps a content with fieldset wrapper
*
* @param string $content
* @return string
*/
protected function getWrappedFieldset($content)
{
return CHtml::tag($this->parentWidget->fieldsetWrapper['tag'],
$this->parentWidget->fieldsetWrapper['htmlOptions'], $content);
}
/**
* Returns the generated label from Yii form builder
* Needs to be replaced by the real attributeLabel
* @see method renderFormElements()
*
* @param string $prefix
* @param string $attributeName
* @return string
*/
protected function getAutoCreatedLabel($prefix, $attributeName)
{
return ($this->model->generateAttributeLabel('[' . $prefix . '][' . $this->index . ']' . $attributeName));
}
/**
* Renders the table head
*
* @return string
*/
public function renderTableHeader()
{
$cells = '';
foreach ($this->getElements() as $element)
if ($element->visible)
{
$text = empty($element->label) ? '&nbsp;' : $element->label;
$options = array();
if ($element->getRequired())
{
$options = array('class' => CHtml::$requiredCss);
$text .= CHtml::$afterRequiredLabel;
}
$cells .= CHtml::tag('th', $options, $text);
}
//add an empty column instead of remove link
$cells .= CHtml::tag('th', array(), '&nbsp');
$row = $this->getWrappedFieldset($cells);
echo CHtml::tag('thead', array(), $cells);
}
/**
* Check if elem is a array type
*
* @param string $type
* @return boolean
*/
protected function isElementArrayType($type)
{
switch ($type)
{
case 'checkboxlist':
case 'radiolist':
return true;
default:
return false;
} // switch
}
/**
* Renders a single form element
* Remove the '[]' from the label
*
* @return string
*/
protected function renderFormElements()
{
$output = '';
$elements = $this->getElements();
foreach ($elements as $element)
{
if (isset($element->name))
{
$elemName = $element->name;
$elemLabel = $element->renderLabel(); //get the correct/nice label before changing name
$element->label = ''; //no label on $element->render()
if ($this->isCopyTemplate) // new fieldset
{
if ($element->visible)
{
//v.2.2 support for checkboxlist radiolist
//Array types have to be rendered as array in the CopyTemplate
$element->name = $this->isElementArrayType($element->type) ? $elemName . '[][]' : $elemName . '[]';
$elemOutput = ($element->type == 'hidden' || //bugfix: v2.1.1
$this->parentWidget->tableView) ? '' : $elemLabel;
$elemOutput .= $element->render();
//bugfix: v2.1 - don't render hidden inputs in table cell
$output .= $element->type == 'hidden' ? $elemOutput : $this->getWrappedRow($elemOutput);
}
}
elseif (!empty($this->primaryKey))
{ // existing fieldsets update
$prefix = 'u__';
$element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName;
if ($element->type == 'hidden')
$output .= $element->render();
else
{
$elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
$elemOutput .= $element->render();
$output .= $this->getWrappedRow($elemOutput);
}
}
else
{ //in validation error mode: the new added items before
if ($element->visible)
{
$prefix = 'n__';
$element->name = '[' . $prefix . '][' . $this->index . ']' . $elemName;
if ($element->type == 'hidden')
$output .= $element->render();
else
{
$elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
$elemOutput .= $element->render();
$output .= $this->getWrappedRow($elemOutput);
}
}
}
}
}
if (!$this->isCopyTemplate)
$output .= $this->parentWidget->getRemoveLink();
return $output;
}
/**
* Renders the primary key value as hidden field
* Need determine which records to delete
*
* @param string $classSuffix
* @return string
*/
public function renderHiddenPk($classSuffix = '[pk__]')
{
foreach ($this->primaryKey as $key => $value)
{
$modelClass = get_class($this->parentWidget->model);
$name = $modelClass . $classSuffix . '[' . $this->index . ']' . '[' . $key . ']';
return CHtml::hiddenField($name, $value);
}
}
/**
* Get the add item link
*
* @return string
*/
public function getAddLink()
{
return CHtml::tag('a',
array('id' => $this->parentWidget->id,
'href' => '#',
'rel' => '.' . $this->parentWidget->getCopyClass()
),
$this->parentWidget->addItemText
);
}
/**
* Renders the link 'Add' for cloning the DOM element
*
* @return string
*/
public function renderAddLink()
{
return $this->getWrappedRow($this->getAddLink());
}
/**
* Renders the CForm
* Each fieldset is wrapped with the fieldsetWrapper
*
* @return string
*/
public function render()
{
$elemOutput = $this->renderBegin();
$elemOutput .= $this->renderFormElements();
$elemOutput .= $this->renderEnd();
// wrap $elemOutput
$wrapperClass = $this->parentWidget->fieldsetWrapper['htmlOptions']['class'];
if ($this->isCopyTemplate)
{
$class = empty($wrapperClass)
? $this->parentWidget->getCopyClass()
: $wrapperClass . ' ' . $this->parentWidget->getCopyClass();
}
else
$class = $wrapperClass;
$this->parentWidget->fieldsetWrapper['htmlOptions']['class'] = $class;
return $this->getWrappedFieldset($elemOutput);
}
}
/**
* MultiModelEmbeddedForm
*
* A CActiveForm with no output of the form begin and close tag
* In Yii 1.1.6 the form end/close is the only output of the methods init() and run()
* Needs review in upcoming releases
*
*/
class MultiModelEmbeddedForm extends CActiveForm
{
/**
* Initializes the widget.
* Don't render the open tag
*/
public function init()
{
ob_start();
parent::init();
ob_get_clean();
}
/**
* Runs the widget.
* Don't render the close tag
*/
public function run()
{
ob_start();
parent::run();
ob_get_clean();
}
}