PageRenderTime 91ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/application/protected/extensions/multimodelform/MultiModelForm.php

https://bitbucket.org/dinhtrung/yiicorecms/
PHP | 1013 lines | 525 code | 155 blank | 333 comment | 88 complexity | 397f33438bd4ef54647303e5a24d7e95 MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause, CC0-1.0, BSD-2-Clause, GPL-2.0, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * MultiModelForm.php
  4. *
  5. * Handling of multiple records and models in a form
  6. *
  7. * Uses the jQuery plugin RelCopy
  8. * @link http://www.andresvidal.com/labs/relcopy.html
  9. *
  10. * @author Joe Blocher <yii@myticket.at>
  11. * @copyright 2011 myticket it-solutions gmbh
  12. * @license New BSD License
  13. * @category User Interface
  14. * @version 2.1
  15. */
  16. class MultiModelForm extends CWidget
  17. {
  18. const CLASSPREFIX = 'mmf_'; //prefix for tag classes
  19. /**
  20. * The model to handle
  21. *
  22. * @var CModel $model
  23. */
  24. public $model;
  25. /**
  26. * Configuration of the form provided by the models method getMultiModelForm()
  27. *
  28. * This configuration array defines generation CForm
  29. * Can be a config array or a config file that returns the configuration
  30. *
  31. * @link http://www.yiiframework.com/doc/guide/1.1/en/form.builder
  32. * @var mixed $elements
  33. */
  34. public $formConfig = array();
  35. /**
  36. * Array of models loaded from db.
  37. * Created for example by $model->findAll();
  38. *
  39. * @var CModel $data
  40. */
  41. public $data;
  42. /**
  43. * The controller returns all validated items (array of model)
  44. * if a validation error occurs.
  45. * The form will then be rendered with error output.
  46. * $data will be ignored in this case.
  47. * @see method run()
  48. *
  49. * @var array $validatedItems
  50. */
  51. public $validatedItems;
  52. /**
  53. * Set to true if the error summary should be rendered for the model of this form
  54. *
  55. * @var boolean $showErrorSummary
  56. */
  57. public $showErrorSummary = false;
  58. /**
  59. * The text of the copy/clone link
  60. *
  61. * @var string $addItemText
  62. */
  63. public $addItemText = 'Add item';
  64. /**
  65. * The text for the remove link
  66. * Can be an image tag too.
  67. * Leave empty to disable removing.
  68. *
  69. * @var string $removeText
  70. */
  71. public $removeText = 'Remove';
  72. /**
  73. * The confirmation text before remove an item
  74. * Set to null/empty to disable confirmation
  75. *
  76. * @var string $removeText
  77. */
  78. public $removeConfirm = 'Delete this item?';
  79. /**
  80. * The htmlOptions for the remove link
  81. *
  82. * @var array $removeHtmlOptions
  83. */
  84. public $removeHtmlOptions = array();
  85. /**
  86. * Show elements as table
  87. * If set to true, $fieldsetWrapper, $rowWrapper and $removeLinkWrapper will be ignored
  88. *
  89. * @var CModel $model
  90. */
  91. public $tableView = false;
  92. /**
  93. * The htmlOptions for the table tag
  94. *
  95. * @var array $tableHtmlOptions
  96. */
  97. public $tableHtmlOptions = array();
  98. /**
  99. * Items are rendered as <tfoot><tr><td>Item1</td><td>Item2</td> ...</tr></tfoot>
  100. *
  101. * @var string $tableFootCells
  102. */
  103. public $tableFootCells = array();
  104. /**
  105. * The wrapper for each fieldset
  106. *
  107. * @var array $fieldsetWrapper
  108. */
  109. public $fieldsetWrapper = array (
  110. 'tag' => 'div',
  111. 'htmlOptions' => array('class' => 'view'), //'fieldset' is unknown in the default css context of form.css
  112. );
  113. /**
  114. * The wrapper for a row
  115. *
  116. * @var array $rowWrapper
  117. */
  118. public $rowWrapper = array (
  119. 'tag' => 'div',
  120. 'htmlOptions' => array('class' => 'row'),
  121. );
  122. /**
  123. * The wrapper for the removeLink
  124. *
  125. * @var array $fieldsetWrapper
  126. */
  127. public $removeLinkWrapper = array (
  128. 'tag' => 'span',
  129. 'htmlOptions' => array(),
  130. );
  131. /**
  132. * The javascript code jsBeforeClone,jsAfterClone ...
  133. * This allows to handle widgets on cloning.
  134. * Important: 'this' is the current handled jQuery object
  135. * For CJuiDatePicker and extension 'datetimepicker' see prepared php-code below: afterNewIdDatePicker,afterNewIdDateTimePicker
  136. *
  137. * Usage if you have CJuiDatePicker to clone (assume your form elements are defined in the array $formConfig):
  138. * 'jsAfterNewId' => MultiModelForm::afterNewIdDateTimePicker($formConfig['elements']['mydatefield']),
  139. *
  140. */
  141. public $jsBeforeClone; // 'jsBeforeClone' => "alert(this.attr('class'));";
  142. public $jsAfterClone; // 'jsAfterClone' => "alert(this.attr('class'));";
  143. public $jsBeforeNewId; // 'jsBeforeNewId' => "alert(this.attr('id'));";
  144. public $jsAfterNewId; // 'jsAfterNewId' => "alert(this.attr('id'));";
  145. /**
  146. * Available options for the jQuery plugin RelCopy
  147. *
  148. * string excludeSelector - A jQuery selector used to exclude an element and its children
  149. * integer limit - The number of allowed copies. Default: 0 is unlimited
  150. * string append - Additional HTML to attach at the end of each copy.
  151. * string copyClass - A class to attach to each copy
  152. * boolean clearInputs - Option to clear each copies text input fields or textarea
  153. *
  154. * @link http://www.andresvidal.com/labs/relcopy.html
  155. *
  156. * @var array $options
  157. */
  158. public $options = array();
  159. /**
  160. * The assets url
  161. *
  162. * @var string $_assets
  163. */
  164. private $_assets;
  165. /**
  166. * Support for CJuiDatePicker
  167. * Set 'jsAfterNewId'=MultiModelForm::afterNewIdDateTimePicker($myFormConfig['elements']['mydate'])
  168. * if you use at least one datepicker.
  169. *
  170. * The options will be assigned from the config array of the element
  171. *
  172. * @param array $element
  173. * @return string
  174. */
  175. public static function afterNewIdDatePicker($element)
  176. {
  177. $options = isset($element['options']) ? $element['options'] : array();
  178. $jsOptions = CJavaScript::encode($options);
  179. $language = isset($element['language']) ? $element['language'] : '';
  180. if (!empty($language))
  181. $language = "jQuery.datepicker.regional['$language'],";
  182. return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker'); this.datepicker(jQuery.extend({showMonthAfterYear:false}, $language {$jsOptions}));};";
  183. }
  184. /**
  185. * Support for extension datetimepicker
  186. * @link http://www.yiiframework.com/extension/datetimepicker/
  187. *
  188. * @param array $element
  189. * @return string
  190. */
  191. public static function afterNewIdDateTimePicker($element)
  192. {
  193. $options = isset($element['options']) ? $element['options'] : array();
  194. $jsOptions = CJavaScript::encode($options);
  195. $language = isset($element['language']) ? $element['language'] : '';
  196. if (!empty($language))
  197. $language = "jQuery.datepicker.regional['$language'],";
  198. return "if(this.hasClass('hasDatepicker')) {this.removeClass('hasDatepicker').datetimepicker(jQuery.extend($language {$jsOptions}));};";
  199. }
  200. /**
  201. * Support for CJuiAutoComplete: not working - needs review
  202. *
  203. * @param array $element
  204. * @return
  205. */
  206. public static function afterNewIdAutoComplete($element)
  207. {
  208. $options = isset($element['options']) ? $element['options'] : array();
  209. if(isset($element['sourceUrl']))
  210. $options['source']=CHtml::normalizeUrl($element['sourceUrl']);
  211. else
  212. $options['source']=$element['source'];
  213. $jsOptions = CJavaScript::encode($options);
  214. //return "this.autocomplete($jsOptions);"; //works for non-autocomplete elements
  215. //return "if(this.hasClass('ui-autocomplete-input')) this.autocomplete($jsOptions);";
  216. //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete($jsOptions);";
  217. //return "if(this.hasClass('ui-autocomplete-input')) $('#'+this.attr('id')).autocomplete('destroy').autocomplete($jsOptions);";
  218. //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);";
  219. //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);";
  220. }
  221. /**
  222. * This static function should be used in the controllers update action
  223. * The models will be validated before saving
  224. *
  225. * If a record is not valid, the invalid model will be set to $model
  226. * to display error summary
  227. *
  228. * @param mixed $model CActiveRecord or other CModel
  229. * @param array $validatedItems returns the array of validated records
  230. * @param array $masterValues attributes to assign before saving
  231. * @param array $formData (default = $_POST)
  232. * @return boolean
  233. */
  234. public static function save($model,&$validatedItems, &$deleteItems=array(), $masterValues = array() ,$formData = null)
  235. {
  236. //validate if empty: means no validation has been done
  237. $doValidate = empty($validatedItems) && empty($deleteItems);
  238. if ($doValidate)
  239. {
  240. //validate and assign $masterValues
  241. if (!self::validate($model, $validatedItems, $deleteItems, $masterValues , $formData))
  242. return false;
  243. }
  244. if (!empty($validatedItems))
  245. foreach ($validatedItems as $item)
  246. {
  247. if (!$doValidate) //assign $masterValues
  248. {
  249. if (!empty($masterValues))
  250. $item->setAttributes($masterValues,false);
  251. }
  252. if (!$item->save())
  253. return false;
  254. }
  255. //$deleteItems = array of primary keys to delete
  256. if (!empty($deleteItems))
  257. foreach($deleteItems as $pk)
  258. if (!empty($pk))
  259. {
  260. //array doesn't work with activerecord?
  261. if (count($pk == 1))
  262. {
  263. $vals = array_values($pk);
  264. $pk = $vals[0];
  265. }
  266. $model->deleteByPk($pk);
  267. }
  268. return true;
  269. }
  270. /**
  271. * Validates submitted formdata
  272. * If a record is not valid, the invalid model will be set to $model
  273. * to display error summary
  274. *
  275. * @param mixed $model
  276. * @param array $validatedItems returns the array of validated records
  277. * @param array $deleteItems returns the array of model for deleting
  278. * @param array $masterValues attributes to assign before saving
  279. * @param array $formData (default = $_POST)
  280. * @return boolean
  281. */
  282. public static function validate($model,&$validatedItems, &$deleteItems=array(), $masterValues = array(), $formData = null)
  283. {
  284. $widget = new MultiModelForm;
  285. $widget->model = $model;
  286. $widget->checkModel();
  287. if (!$widget->initItems($validatedItems, $deleteItems, $masterValues, $formData))
  288. return false; //at least one item is not valid
  289. else
  290. return true;
  291. }
  292. /**
  293. * Converts the submitted formdata into an array of model
  294. *
  295. * @param array $formData the postdata $_POST submitted by the form
  296. * @param array $validatedItems the items which were validated
  297. * @param array $deleteItems the items to delete
  298. * @param array $masterValues assign additional masterdata before save
  299. * @return array array of model
  300. */
  301. public function initItems(&$validatedItems,&$deleteItems, $masterValues = array(),$formData = null)
  302. {
  303. if (!isset($formData)){
  304. $formData = $_POST;
  305. }
  306. $result = true;
  307. $newItems = array();
  308. $validatedItems = array(); //bugfix: 1.0.2
  309. $deleteItems = array();
  310. $modelClass = get_class($this->model);
  311. if (!isset($formData) || empty($formData[$modelClass]))
  312. return true;
  313. //----------- NEW (on validation error) -----------
  314. if (isset($formData[$modelClass]['n__']))
  315. {
  316. foreach ($formData[$modelClass]['n__'] as $idx => $attributes)
  317. {
  318. $model = new $modelClass;
  319. $model->attributes = $attributes;
  320. if (!empty($masterValues))
  321. $model->setAttributes($masterValues,false);
  322. foreach ($model->getAttributes() as $attr => $orig){
  323. $files = CUploadedFile::getInstance($model, "[n__][$idx]$attr");
  324. if (is_null($files)) continue;
  325. $model->$attr = $files;
  326. }
  327. // validate
  328. if (!$model->validate())
  329. $result = false;
  330. $validatedItems[] = $model;
  331. }
  332. unset($formData[$modelClass]['n__']);
  333. }
  334. //----------- UPDATE -----------
  335. $allExistingPk = isset($formData[$modelClass]['pk__']) ? $formData[$modelClass]['pk__'] : null; //bugfix: 1.0.1
  336. if (isset($formData[$modelClass]['u__']))
  337. {
  338. foreach ($formData[$modelClass]['u__'] as $idx => $attributes)
  339. {
  340. $model = new $modelClass('update');
  341. //should work for CModel, mongodb models... too
  342. if (method_exists($model,'setIsNewRecord'))
  343. $model->setIsNewRecord(false);
  344. $model->setAttributes($attributes);
  345. foreach ($model->getAttributes() as $attr => $orig){
  346. $files = CUploadedFile::getInstance($model, "[u__][$idx]$attr");
  347. if (is_null($files)) continue;
  348. $model->$attr = $files;
  349. }
  350. if (!empty($masterValues))
  351. $model->setAttributes($masterValues,false); //assign mastervalues
  352. //ensure to assign primary keys (when pk is unsafe or not defined in rules)
  353. if (is_array($allExistingPk))
  354. {
  355. $primaryKeys = $allExistingPk[$idx];
  356. $model->setAttributes($primaryKeys,false);
  357. }
  358. // validate
  359. if (!$model->validate())
  360. $result = false;
  361. $validatedItems[] = $model;
  362. // remove from $allExistingPk
  363. if (is_array($allExistingPk))
  364. unset($allExistingPk[$idx]);
  365. }
  366. unset($formData[$modelClass]['u__']);
  367. }
  368. //----------- DELETE -----------
  369. // add remaining primarykeys to $deleteItems (reindex)
  370. if (is_array($allExistingPk))
  371. foreach ($allExistingPk as $idx => $delPks)
  372. $deleteItems[] = $delPks;
  373. // remove handled formdata pk__
  374. unset($formData[$modelClass]['pk__']);
  375. //----------- ADDED by jQuery -----------
  376. // use the first item as reference
  377. $model = new $modelClass;
  378. $refAttribute = key($formData[$modelClass]);
  379. $refArray = array_shift($formData[$modelClass]);
  380. foreach ($model->getAttributes(TRUE) as $attr => $val){
  381. $files[$attr] = CUploadedFile::getInstances($model, $attr);
  382. }
  383. unset($model);
  384. foreach($refArray as $idx => $value)
  385. {
  386. // check continue if all values are empty
  387. if (empty($value))
  388. {
  389. $allEmpty = true;
  390. foreach ($formData[$modelClass] as $attrKey => $values)
  391. {
  392. $allEmpty = empty($values[$idx]) && $allEmpty;
  393. if (!$allEmpty)
  394. break;
  395. }
  396. if ($allEmpty)
  397. continue;
  398. }
  399. $model = new $modelClass;
  400. $model->$refAttribute = $value;
  401. foreach ($formData[$modelClass] as $attrKey => $values){
  402. $model->$attrKey = $values[$idx];
  403. }
  404. //assign mastervalues without checking rules for new records
  405. $model->setAttributes($masterValues,false);
  406. foreach ($model->getAttributes(TRUE) as $attr => $orig){
  407. if (empty($files[$attr][$idx])) continue;
  408. $model->$attr = $files[$attr][$idx];
  409. }
  410. // validate
  411. if (!$model->validate())
  412. $result = false;
  413. $validatedItems[] = $model;
  414. }
  415. return $result;
  416. }
  417. /**
  418. * Get the primary key as array (key => value)
  419. *
  420. * @param CModel $model
  421. * @return array
  422. */
  423. public function getPrimaryKey($model)
  424. {
  425. $result = array();
  426. if ($model instanceof CActiveRecord)
  427. {
  428. $pkValue = $model->primaryKey;
  429. if (!empty($pkValue))
  430. {
  431. $pkName = $model->primaryKey();
  432. if (empty($pkName))
  433. $pkName = $model->tableSchema->primaryKey;
  434. $result = array($pkName => $pkValue);
  435. }
  436. }
  437. else // when working with EMongoDocument
  438. if (method_exists($model, 'primaryKey'))
  439. {
  440. $pkName = $model->primaryKey();
  441. $pkValue = $model->$pkName;
  442. if (empty($pkValue))
  443. $result = array($pkName => $pkValue);
  444. }
  445. return $result;
  446. }
  447. /**
  448. * Get the copyClass
  449. *
  450. * @return string
  451. */
  452. public function getCopyClass()
  453. {
  454. if (isset($this->options['copyClass']))
  455. return $this->options['copyClass'];
  456. else
  457. {
  458. $selector = $this->id . '_copy';
  459. $this->options['copyClass'] = $selector;
  460. return $selector;
  461. }
  462. }
  463. /**
  464. * The link for removing a fieldset
  465. *
  466. * @return string
  467. */
  468. public function getRemoveLink()
  469. {
  470. if (empty($this->removeText))
  471. return '';
  472. $onClick = '$(this).parent().parent().remove(); return false;';
  473. if (!empty($this->removeConfirm))
  474. $onClick = "if(confirm('{$this->removeConfirm}')) " . $onClick;
  475. $htmlOptions = array_merge($this->removeHtmlOptions, array('onclick' => $onClick));
  476. $link = CHtml::link($this->removeText, '#', $htmlOptions);
  477. return CHtml::tag($this->removeLinkWrapper['tag'],
  478. $this->removeLinkWrapper['htmlOptions'], $link);
  479. }
  480. /**
  481. * Initialize the widget: register scripts
  482. */
  483. public function init()
  484. {
  485. if ($this->tableView)
  486. {
  487. $this->fieldsetWrapper = array('tag' => 'tr','htmlOptions'=>array('class'=>self::CLASSPREFIX.'row'));
  488. $this->rowWrapper = array('tag' => 'td','htmlOptions'=>array('class'=>self::CLASSPREFIX.'cell'));
  489. $this->removeLinkWrapper = $this->rowWrapper;
  490. }
  491. $this->checkModel();
  492. $this->registerClientScript();
  493. parent::init();
  494. }
  495. /**
  496. * Check the model instance on init / after create
  497. */
  498. protected function checkModel()
  499. {
  500. if (is_string($this->model))
  501. $this->model = new $this->model;
  502. if (isset($this->model) && isset($this->formConfig))
  503. {
  504. // add undefined attributes in the form config as hidden fields
  505. if (isset($this->formConfig) && !empty($this->formConfig['elements']))
  506. foreach ($this->model->attributes as $attribute => $value)
  507. {
  508. if (!array_key_exists($attribute, $this->formConfig['elements']))
  509. $this->formConfig['elements'][$attribute] = array('type' => 'hidden');
  510. }
  511. }
  512. }
  513. /**
  514. * @return array the javascript options
  515. */
  516. protected function getClientOptions()
  517. {
  518. if (empty($this->options))
  519. $this->options = array();
  520. if (!empty($this->removeText))
  521. {
  522. $append = $this->getRemoveLink();
  523. $this->options['append'] = empty($this->options['append']) ? $append : $append . ' ' . $this->options['append'];
  524. }
  525. if (!empty($this->jsBeforeClone))
  526. $this->options['beforeClone'] = $this->jsBeforeClone;
  527. if (!empty($this->jsAfterClone))
  528. $this->options['afterClone'] = $this->jsAfterClone;
  529. if (!empty($this->jsBeforeNewId))
  530. $this->options['beforeNewId'] = $this->jsBeforeNewId;
  531. if (!empty($this->jsAfterNewId))
  532. $this->options['afterNewId'] = $this->jsAfterNewId;
  533. return CJavaScript::encode($this->options);
  534. }
  535. /**
  536. * Registers the relcopy javascript file.
  537. */
  538. public function registerClientScript()
  539. {
  540. $this->_assets = Yii::app()->assetManager->publish(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'assets');
  541. Yii::app()->clientScript->registerCoreScript('jquery')->registerScriptFile($this->_assets . '/js/jquery.relcopy.yii.1.0.js');
  542. $options = $this->getClientOptions();
  543. Yii::app()->getClientScript()->registerScript(__CLASS__ . '#' . $this->id, "jQuery('#{$this->id}').relCopy($options);");
  544. }
  545. /**
  546. * Render the top of the table: AddLink, Table header
  547. */
  548. public function renderTableBegin()
  549. {
  550. $form = new MultiModelRenderForm($this->formConfig, $this->model);
  551. $form -> parentWidget = $this;
  552. //add link as div
  553. $addLink = $form->getAddLink();
  554. echo CHtml::tag('div',array('class'=>self::CLASSPREFIX.'addlink'), $addLink);
  555. $tableHtmlOptions = array_merge(array('class'=>self::CLASSPREFIX.'table'),$this->tableHtmlOptions);
  556. //table
  557. echo CHtml::tag('table',$tableHtmlOptions,false,false);
  558. //thead
  559. $form -> renderTableHeader();
  560. //tfoot
  561. if (!empty($this->tableFootCells))
  562. {
  563. $cells = '';
  564. foreach ($this->tableFootCells as $cell)
  565. {
  566. $cells .= CHtml::tag('td',array('class'=>self::CLASSPREFIX.'cell'),$cell);
  567. }
  568. $cells = CHtml::tag('tr',array('class'=>self::CLASSPREFIX.'row'),$cells);
  569. echo CHtml::tag('tfoot',array(),$cells);
  570. }
  571. //tbody
  572. echo CHtml::tag('tbody',array(),false,false);
  573. }
  574. /**
  575. * Renders the active form if a model and formConfig is set
  576. * $this->data is array of model
  577. */
  578. public function run()
  579. {
  580. if (empty($this->model) || empty($this->formConfig))
  581. return;
  582. $this->formConfig['activeForm'] = array('class' => 'MultiModelEmbeddedForm');
  583. $idx = 0;
  584. $errorPk = null;
  585. if (!empty($this->validatedItems)) //some invalid models exists
  586. {
  587. if ($this->showErrorSummary)
  588. echo CHtml::errorSummary($this->validatedItems);
  589. $data = $this->validatedItems;
  590. }
  591. else
  592. $data = $this->data;
  593. if ($this->tableView)
  594. $this->renderTableBegin();
  595. // existing records
  596. if (is_array($data) && !empty($data))
  597. {
  598. foreach ($data as $model)
  599. {
  600. $form = new MultiModelRenderForm($this->formConfig, $model);
  601. $form->index = $idx;
  602. $form->parentWidget = $this;
  603. $form->primaryKey = $this->getPrimaryKey($model);
  604. if (!$this->tableView)
  605. {
  606. if ($idx == 0) // no existing data rendered
  607. echo $form->renderAddLink();
  608. }
  609. // render pk outside of removeable tag, for checking records to delete
  610. // @see method initItems()
  611. echo $form->renderHiddenPk('[pk__]');
  612. echo $form->render();
  613. $idx++;
  614. }
  615. }
  616. // add an empty fieldset as CopyTemplate
  617. $form = new MultiModelRenderForm($this->formConfig, $this->model);
  618. $form->index = $idx;
  619. $form->parentWidget = $this;
  620. $form->isCopyTemplate = true;
  621. if (!$this->tableView)
  622. {
  623. if ($idx == 0) // no existing data rendered
  624. echo $form->renderAddLink();
  625. }
  626. echo $form->render();
  627. if ($this->tableView)
  628. {
  629. echo CHtml::closeTag('tbody');
  630. echo CHtml::closeTag('table');
  631. }
  632. }
  633. }
  634. /**
  635. * The CForm to render the input form
  636. */
  637. class MultiModelRenderForm extends CForm
  638. {
  639. public $parentWidget;
  640. public $index;
  641. public $isCopyTemplate;
  642. public $primaryKey;
  643. private $_hiddenElements;
  644. /**
  645. * Wraps a content with row wrapper
  646. *
  647. * @param string $content
  648. * @return string
  649. */
  650. protected function getWrappedRow($content)
  651. {
  652. return CHtml::tag($this->parentWidget->rowWrapper['tag'],
  653. $this->parentWidget->rowWrapper['htmlOptions'], $content);
  654. }
  655. /**
  656. * Wraps a content with fieldset wrapper
  657. *
  658. * @param string $content
  659. * @return string
  660. */
  661. protected function getWrappedFieldset($content)
  662. {
  663. return CHtml::tag($this->parentWidget->fieldsetWrapper['tag'],
  664. $this->parentWidget->fieldsetWrapper['htmlOptions'], $content);
  665. }
  666. /**
  667. * Returns the generated label from Yii form builder
  668. * Needs to be replaced by the real attributeLabel
  669. * @see method renderFormElements()
  670. *
  671. * @param string $prefix
  672. * @param string $attributeName
  673. * @return
  674. */
  675. protected function getAutoCreatedLabel($prefix,$attributeName)
  676. {
  677. return ($this->model->generateAttributeLabel('['.$prefix.'][' . $this->index . ']'.$attributeName));
  678. }
  679. /**
  680. * Renders the table head
  681. *
  682. * @return
  683. */
  684. public function renderTableHeader()
  685. {
  686. $cells = '';
  687. foreach($this->getElements() as $element)
  688. if ($element->visible && ($element->type != 'hidden'))
  689. {
  690. $text = empty($element->label) ? '&nbsp;' : $element->label;
  691. $options = array();
  692. if ($element->getRequired())
  693. {
  694. $options = array('class'=>CHtml::$requiredCss);
  695. $text .= CHtml::$afterRequiredLabel;
  696. }
  697. $cells .= CHtml::tag('th',$options,$text);
  698. }
  699. //add an empty column instead of remove link
  700. $cells .= CHtml::tag('th',array(),'&nbsp');
  701. $row = $this->getWrappedFieldset($cells);
  702. echo CHtml::tag('thead',array(),$cells);
  703. }
  704. /**
  705. * Renders a single form element
  706. * Remove the '[]' from the label
  707. *
  708. * @return string
  709. */
  710. protected function renderFormElements()
  711. {
  712. $output = '';
  713. $elements = $this->getElements();
  714. foreach($elements as $element)
  715. {
  716. if (isset($element->name))
  717. {
  718. $elemName = $element->name;
  719. if ($element->type != 'hidden')
  720. $elemLabel = $element->renderLabel(); //get the correct/nice label before changing name
  721. else
  722. $elemLabel = '';
  723. $element->label = ''; //no label on $element->render()
  724. if ($this->isCopyTemplate)// new fieldset
  725. {
  726. $element->name = $elemName . '[]';
  727. $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
  728. $elemOutput .= $element->render();
  729. //bugfix: v2.1 - don't render hidden inputs in table cell
  730. $output .= $element->type == 'hidden' ? $elemOutput :$this->getWrappedRow($elemOutput);
  731. }
  732. elseif(!empty($this->primaryKey))
  733. { // existing fieldsets update
  734. $prefix = 'u__';
  735. $element->name = '['.$prefix.'][' . $this->index . ']' . $elemName;
  736. if ($element->type == 'hidden')
  737. $output .= $element->render();
  738. else
  739. {
  740. $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
  741. $elemOutput .= $element->render();
  742. $output .= $this->getWrappedRow($elemOutput);
  743. }
  744. }
  745. else
  746. { //in validation error mode: the new added items before
  747. $prefix = 'n__';
  748. $element->name = '['.$prefix.'][' . $this->index . ']' . $elemName;
  749. if ($element->type == 'hidden')
  750. $output .= $element->render();
  751. else
  752. {
  753. $elemOutput = $this->parentWidget->tableView ? '' : $elemLabel;
  754. $elemOutput .= $element->render();
  755. $output .= $this->getWrappedRow($elemOutput);
  756. }
  757. }
  758. }
  759. }
  760. if (!$this->isCopyTemplate)
  761. $output .= $this->parentWidget->getRemoveLink();
  762. return $output;
  763. }
  764. /**
  765. * Renders the primary key value as hidden field
  766. * Need determine which records to delete
  767. *
  768. * @return string
  769. */
  770. public function renderHiddenPk($classSuffix = '[pk__]')
  771. {
  772. foreach ($this->primaryKey as $key => $value)
  773. {
  774. $modelClass = get_class($this->parentWidget->model);
  775. $name = $modelClass . $classSuffix . '[' . $this->index . ']' . '[' . $key . ']';
  776. return CHtml::hiddenField($name, $value);
  777. }
  778. }
  779. /**
  780. * Get the add item link
  781. *
  782. * @return string
  783. */
  784. public function getAddLink()
  785. {
  786. return CHtml::tag('a',
  787. array('id' => $this->parentWidget->id,
  788. 'href' => '#',
  789. 'rel' => '.' . $this->parentWidget->getCopyClass()
  790. ),
  791. $this->parentWidget->addItemText
  792. );
  793. }
  794. /**
  795. * Renders the link 'Add' for cloning the DOM element
  796. *
  797. * @return string
  798. */
  799. public function renderAddLink()
  800. {
  801. return $this->getWrappedRow($this->getAddLink());
  802. }
  803. /**
  804. * Renders the CForm
  805. * Each fieldset is wrapped with the fieldsetWrapper
  806. *
  807. * @return string
  808. */
  809. public function render()
  810. {
  811. $elemOutput = $this->renderBegin();
  812. $elemOutput .= $this->renderFormElements();
  813. $elemOutput .= $this->renderEnd();
  814. // wrap $elemOutput
  815. $wrapperClass = $this->parentWidget->fieldsetWrapper['htmlOptions']['class'];
  816. if ($this->isCopyTemplate)
  817. {
  818. $class = empty($wrapperClass)
  819. ? $this->parentWidget->getCopyClass()
  820. : $wrapperClass . ' ' . $this->parentWidget->getCopyClass();
  821. }
  822. else
  823. $class = $wrapperClass;
  824. $this->parentWidget->fieldsetWrapper['htmlOptions']['class'] = $class;
  825. return $this->getWrappedFieldset($elemOutput);
  826. }
  827. }
  828. /**
  829. * MultiModelEmbeddedForm
  830. *
  831. * A CActiveForm with no output of the form begin and close tag
  832. * In Yii 1.1.6 the form end/close is the only output of the methods init() and run()
  833. * Needs review in upcoming releases
  834. *
  835. */
  836. class MultiModelEmbeddedForm extends CActiveForm
  837. {
  838. /**
  839. * Initializes the widget.
  840. * Don't render the open tag
  841. */
  842. public function init()
  843. {
  844. ob_start();
  845. parent::init();
  846. ob_get_clean();
  847. }
  848. /**
  849. * Runs the widget.
  850. * Don't render the close tag
  851. */
  852. public function run()
  853. {
  854. ob_start();
  855. parent::run();
  856. ob_get_clean();
  857. }
  858. }