diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..b263871
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,10 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.travis.yml export-ignore
+/phpunit.xml.dist export-ignore
+/.scrutinizer.yml export-ignore
+/tests export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f308e6f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+build
+docs
+vendor
+
+# cache directories
+Thumbs.db
+*.DS_Store
+*.empty
+
+#phpstorm project files
+.idea
+
+#netbeans project files
+nbproject
+
+#eclipse, zend studio, aptana or other eclipse like project files
+.buildpath
+.project
+.settings
+
+# composer itself is not needed
+composer.phar
+composer.lock
+
+# mac deployment helpers
+switch
+index
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..cf46d83
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,35 @@
+filter:
+ excluded_paths: [tests/*]
+checks:
+ php:
+ code_rating: true
+ remove_extra_empty_lines: true
+ remove_php_closing_tag: true
+ remove_trailing_whitespace: true
+ fix_use_statements:
+ remove_unused: true
+ preserve_multiple: false
+ preserve_blanklines: true
+ order_alphabetically: true
+ fix_php_opening_tag: true
+ fix_linefeed: true
+ fix_line_ending: true
+ fix_identation_4spaces: true
+ fix_doc_comments: true
+tools:
+ external_code_coverage:
+ timeout: 600
+ runs: 3
+ php_analyzer: true
+ php_code_coverage: false
+ php_code_sniffer:
+ config:
+ standard: PSR2
+ filter:
+ paths: ['src']
+ php_loc:
+ enabled: true
+ excluded_dirs: [vendor, tests]
+ php_cpd:
+ enabled: true
+ excluded_dirs: [vendor, tests]
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..8722f56
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+language: php
+
+php:
+ - 5.4
+ - 5.5
+ - 5.6
+ - 7.0
+ - hhvm
+
+install:
+ - composer self-update
+ - composer global require "fxp/composer-asset-plugin:1.0.0"
+ - composer install
+
+before_script:
+ - travis_retry composer self-update
+ - travis_retry composer install --no-interaction --prefer-source --dev
+
+script:
+ - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
+
+after_script:
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover coverage.clover
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..2daa100
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,47 @@
+# yii2-dynamicform change Log
+
+
+## [v2.0.3 (2020-05-18)](https://github.com/yii2-extensions/dynamicform/compare/v2.0.3...v2.0.2)
+
+- Enh: Updated composer.json ('symfony/dom-crawler': '~2.8|~3.0' and 'symfony/css-selector': '~2.8|~3.0').
+- Bug #40: Fixed dropDownList reset after insert item.
+- Enh #25: Added enhancements to better support for nested widgets.
+- Enh #24: Added support for "jquery.inputmask". It only works with Yii 2.0.4 or higher.
+- Enh: Remove "error/success" class css template to be cloned.
+- Bug: Fixes for: checkbox(), checkboxList(), radio() and radioList().
+- Bug #224: Fixes the cloning of elements.
+
+
+## [v2.0.2 (2015-02-25)](https://github.com/yii2-extensions/dynamicform/compare/v2.0.2...v2.0.1)
+
+- Bug #22: Correct reset attributes (id, name) when we have more than two nested widgets
+
+
+## [v2.0.1 (2015-02-23)](https://github.com/yii2-extensions/dynamicform/compare/v2.0.1...v2.0.0)
+
+- Bug: Fixed error for the use of multiple nested widgets with zero initial elements
+
+
+## v2.0.0 (2015-02-22)
+
+- Enh #20: Added trigger 'beforeDelete'
+- Bug #19: Fixes checkboxes on new items
+- Enh #15: Added support for multiple nested widgets
+
+
+## v1.1.0 (2014-12-16)
+
+- Bug #7: Added support for "kartik-v/yii2-widget-depdrop" for working with type DepDrop::TYPE_SELECT2
+- Bug #8: Fixes to support the latest version of kartik-v widgets
+- Bug: Fixed client validation
+- Bug #6: Fixed settings for datepicker
+- Enh: Added support for "kartik-v/yii2-widget-depdrop"
+- Enh: Added support for "kartik-v/yii2-widget-select2"
+- Bug: Fixed html name attribute for "kartik-v/yii2-widget-colorinput"
+- Enh: Added support for "kartik-v/yii2-widget-timepicker"
+- Enh: Added support for "kartik-v/yii2-widget-colorinput"
+
+
+## v1.0.0 (2014-11-05)
+
+Initial release
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..d276dd6
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,28 @@
+# The BSD License (BSD)
+
+Copyright (c) 2014, Wanderson Bragança
+
+> Redistribution and use in source and binary forms, with or without modification,
+> are permitted provided that the following conditions are met:
+>
+> Redistributions of source code must retain the above copyright notice, this
+> list of conditions and the following disclaimer.
+>
+> Redistributions in binary form must reproduce the above copyright notice, this
+> list of conditions and the following disclaimer in the documentation and/or
+> other materials provided with the distribution.
+>
+> Neither the name of Wanderson Bragança nor the names of its
+> contributors may be used to endorse or promote products derived from
+> this software without specific prior written permission.
+>
+>THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+>ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+>WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+>DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+>ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+>(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+>LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+>ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+>(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+>SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e88b2fb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,366 @@
+# yii2-dynamicform
+
+[![Latest Version](https://img.shields.io/github/release/wbraganca/yii2-dynamicform.svg?style=flat-square)](https://github.com/yii2-extensions/dynamicform/releases)
+[![Software License](http://img.shields.io/badge/license-BSD3-brightgreen.svg?style=flat-square)](LICENSE.md)
+[![Total Downloads](https://img.shields.io/packagist/dt/wbraganca/yii2-dynamicform.svg?style=flat-square)](https://packagist.org/packages/wbraganca/yii2-dynamicform)
+
+
+It is widget to yii2 framework to clone form elements in a nested manner, maintaining accessibility.
+![yii2-dynamicform](https://wbraganca.com/img/yii2-dynamicform/sample.jpg)
+
+## Installation
+
+
+The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
+
+Either run
+
+```
+composer require --prefer-dist yii2-extensions/dynamicform:"^1.0.0"
+```
+
+or add
+
+```
+"yii2-extensions/dynamicform": "^1.0.0"
+```
+
+to the require section of your `composer.json` file.
+
+## Extension Usage
+
+### Databases
+To explain usage of this extension we are going to have a sample scenario where we are building address book for customers.
+Each customer can have multiple addresses. See the image below for further details.
+
+![Database](images/scenario.jpg)
+
+
+### Models
+With that database, our assumption is you have two models `Customer` and `Address` classes.
+
+```php
+load(Yii::$app->request->post())) {
+
+ $modelsAddress = Model::createMultiple(Address::classname());
+ Model::loadMultiple($modelsAddress, Yii::$app->request->post());
+
+ // ajax validation
+ if (Yii::$app->request->isAjax) {
+ Yii::$app->response->format = Response::FORMAT_JSON;
+ return ArrayHelper::merge(
+ ActiveForm::validateMultiple($modelsAddress),
+ ActiveForm::validate($modelCustomer)
+ );
+ }
+
+ // validate all models
+ $valid = $modelCustomer->validate();
+ $valid = Model::validateMultiple($modelsAddress) && $valid;
+
+ if ($valid) {
+ $transaction = \Yii::$app->db->beginTransaction();
+ try {
+ if ($flag = $modelCustomer->save(false)) {
+ foreach ($modelsAddress as $modelAddress) {
+ $modelAddress->customer_id = $modelCustomer->id;
+ if (! ($flag = $modelAddress->save(false))) {
+ $transaction->rollBack();
+ break;
+ }
+ }
+ }
+ if ($flag) {
+ $transaction->commit();
+ return $this->redirect(['view', 'id' => $modelCustomer->id]);
+ }
+ } catch (Exception $e) {
+ $transaction->rollBack();
+ }
+ }
+ }
+
+ return $this->render('create', [
+ 'modelCustomer' => $modelCustomer,
+ 'modelsAddress' => (empty($modelsAddress)) ? [new Address] : $modelsAddress
+ ]);
+ }
+}
+```
+
+#### 2. Update Action
+```php
+findModel($id);
+ $modelsAddress = $modelCustomer->addresses;
+
+ if ($modelCustomer->load(Yii::$app->request->post())) {
+
+ $oldIDs = ArrayHelper::map($modelsAddress, 'id', 'id');
+ $modelsAddress = Model::createMultiple(Address::classname(), $modelsAddress);
+ Model::loadMultiple($modelsAddress, Yii::$app->request->post());
+ $deletedIDs = array_diff($oldIDs, array_filter(ArrayHelper::map($modelsAddress, 'id', 'id')));
+
+ // ajax validation
+ if (Yii::$app->request->isAjax) {
+ Yii::$app->response->format = Response::FORMAT_JSON;
+ return ArrayHelper::merge(
+ ActiveForm::validateMultiple($modelsAddress),
+ ActiveForm::validate($modelCustomer)
+ );
+ }
+
+ // validate all models
+ $valid = $modelCustomer->validate();
+ $valid = Model::validateMultiple($modelsAddress) && $valid;
+
+ if ($valid) {
+ $transaction = \Yii::$app->db->beginTransaction();
+ try {
+ if ($flag = $modelCustomer->save(false)) {
+ if (! empty($deletedIDs)) {
+ Address::deleteAll(['id' => $deletedIDs]);
+ }
+ foreach ($modelsAddress as $modelAddress) {
+ $modelAddress->customer_id = $modelCustomer->id;
+ if (! ($flag = $modelAddress->save(false))) {
+ $transaction->rollBack();
+ break;
+ }
+ }
+ }
+ if ($flag) {
+ $transaction->commit();
+ return $this->redirect(['view', 'id' => $modelCustomer->id]);
+ }
+ } catch (Exception $e) {
+ $transaction->rollBack();
+ }
+ }
+ }
+
+ return $this->render('update', [
+ 'modelCustomer' => $modelCustomer,
+ 'modelsAddress' => (empty($modelsAddress)) ? [new Address] : $modelsAddress
+ ]);
+ }
+}
+```
+
+
+### The View
+The View presents our complex form that will dynamically add or remove items. At the hear of it is the `DynamicFormWidget`
+The following are some details on widget profperties:
+- `widgetContainer`: Top container for the widget. Can only be alphanumeric plus a `_` character. It is required
+- `widgetBody` : The Container that hosts rows of form elements. Its value must conform to css class. It is required
+- `widgetItem` : Represents single row of form line. If you are used to Bootstrap grid, `widgetBody` is similar to a container and `widgetItem` to a row. It is a required element and must be in the format of css class
+- `limit` : Maximum number of clones. It is an integer. Limits the number of times element can be cloned. Defaults to 999.
+- `min` : Minimum number of elements by default. Set it to 0 if you want empty sub-form elements or 1 to start with single row. Defaults to 1.
+- `insertButton` : Css class name for an element when clicked will add a row in the form.
+- `deleteButton` : Css class name for an element when clicked will delete a row in the form.
+- `model` : Sample model for the widget. If you are not sure, pass first element of the model rows. This requires your controller always send at least single model to the view.
+- `formId` : ID of your `ActiveForm`. Mismatching the two is a recipe for disaster. Be careful!
+
+Sample view
+
+```php
+
+
+
+```
+
+### Javascript Events
+
+```javascript
+
+$(".dynamicform_wrapper").on("beforeInsert", function(e, item) {
+ console.log("beforeInsert");
+});
+
+$(".dynamicform_wrapper").on("afterInsert", function(e, item) {
+ console.log("afterInsert");
+});
+
+$(".dynamicform_wrapper").on("beforeDelete", function(e, item) {
+ if (! confirm("Are you sure you want to delete this item?")) {
+ return false;
+ }
+ return true;
+});
+
+$(".dynamicform_wrapper").on("afterDelete", function(e) {
+ console.log("Deleted item!");
+});
+
+$(".dynamicform_wrapper").on("limitReached", function(e, item) {
+ alert("Limit reached");
+});
+
+```
+
+### Special Thanks
+Special thanks to Wanderson Bragança for creating wonderful extension. We are here to make sure that his good work does not die. We are making our best to preserve his identity throughout the code he wrote.
+
+Thank you Wanderson!
+
+Check original work at https://github.com/wbraganca/yii2-dynamicform
+
+### Questions and Contributions
+Contributions are welcome in form of PR. Please make a pull request to our github repository.
+You can ask a question or suggest a feature or file a bug using github issues.
+
+Please star our repositories if you find them useful.
+
+If you are on X, don't forget to connect with us at:
+@yiiframework for Yii Framework
+@yiiupdates for Yii News and Updates
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..9b11c2d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,49 @@
+{
+ "name": "yii2-extensions/dynamicform",
+ "description": "Yii2 Dynamic form widget for cloning form elements in a nested manner while maintaining accessibility.",
+ "keywords": [
+ "yii2",
+ "extension",
+ "widget",
+ "yii2-dynamicform",
+ "copy DOM element",
+ "dynamic form",
+ "Yii2 extensions"
+ ],
+ "type": "yii2-extension",
+ "license": "BSD-3-Clause",
+ "minimum-stability": "dev",
+ "prefer-stable": true,
+ "support": {
+ "issues": "https://github.com/yii2-extensions/dynamicform/issues",
+ "wiki": "https://github.com/yii2-extensions/dynamicform/wiki/",
+ "source": "https://github.com/yii2-extensions/dynamicform"
+ },
+ "authors": [
+ {
+ "name": "Wanderson Bragança",
+ "email": "wanderson.wbc@gmail.com",
+ "homepage": "http://wbraganca.com"
+ },
+ {
+ "name": "Stefano Mtangoo",
+ "email": "mwinjilisti@gmail.com",
+ "homepage": "http://hosannahighertech.co.tz"
+ }
+ ],
+ "require": {
+ "yiisoft/yii2": "^2.0.49",
+ "symfony/css-selector": "^6.4||^7.0",
+ "symfony/dom-crawler": "^6.4||^7.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Yii2\\Extensions\\DynamicForm\\": "src"
+ }
+ },
+ "config": {
+ "allow-plugins": {
+ "yiisoft/yii2-composer": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/images/scenario.jpeg b/images/scenario.jpeg
new file mode 100644
index 0000000..88f1016
Binary files /dev/null and b/images/scenario.jpeg differ
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..c95e24d
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,29 @@
+
+
+
+
+ tests
+
+
+
+
+ src/
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DynamicFormAsset.php b/src/DynamicFormAsset.php
new file mode 100644
index 0000000..7784f3c
--- /dev/null
+++ b/src/DynamicFormAsset.php
@@ -0,0 +1,65 @@
+
+ * @author Stefano Mtangoo
+ */
+class DynamicFormAsset extends \yii\web\AssetBundle
+{
+ /**
+ * @inheritdoc
+ */
+ public $depends = [
+ 'yii\web\JqueryAsset',
+ 'yii\widgets\ActiveFormAsset'
+ ];
+
+ /**
+ * Set up CSS and JS asset arrays based on the base-file names
+ * @param string $type whether 'css' or 'js'
+ * @param array $files the list of 'css' or 'js' basefile names
+ */
+ protected function setupAssets($type, $files = [])
+ {
+ $srcFiles = [];
+ $minFiles = [];
+ foreach ($files as $file) {
+ $srcFiles[] = "{$file}.{$type}";
+ $minFiles[] = "{$file}.min.{$type}";
+ }
+ if (empty($this->$type)) {
+ $this->$type = YII_DEBUG ? $srcFiles : $minFiles;
+ }
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function init()
+ {
+ $this->setSourcePath(__DIR__ . '/assets');
+ $this->setupAssets('js', ['yii2-dynamic-form']);
+ parent::init();
+ }
+
+ /**
+ * Sets the source path if empty
+ * @param string $path the path to be set
+ */
+ protected function setSourcePath($path)
+ {
+ if (empty($this->sourcePath)) {
+ $this->sourcePath = $path;
+ }
+ }
+}
diff --git a/src/DynamicFormWidget.php b/src/DynamicFormWidget.php
new file mode 100644
index 0000000..635ddd1
--- /dev/null
+++ b/src/DynamicFormWidget.php
@@ -0,0 +1,272 @@
+
+ * @author Stefano Mtangoo
+ */
+class DynamicFormWidget extends \yii\base\Widget
+{
+ const WIDGET_NAME = 'dynamicform';
+ /**
+ * @var string
+ */
+ public $widgetContainer;
+ /**
+ * @var string
+ */
+ public $widgetBody;
+ /**
+ * @var string
+ */
+ public $widgetItem;
+ /**
+ * @var string
+ */
+ public $limit = 999;
+ /**
+ * @var string
+ */
+ public $insertButton;
+ /**
+ * @var string
+ */
+ public $deleteButton;
+ /**
+ * @var string 'bottom' or 'top';
+ */
+ public $insertPosition = 'bottom';
+ /**
+ * @var Model|ActiveRecord the model used for the form
+ */
+ public $model;
+ /**
+ * @var string form ID
+ */
+ public $formId;
+ /**
+ * @var array fields to be validated.
+ */
+ public $formFields;
+ /**
+ * @var integer
+ */
+ public $min = 1;
+ /**
+ * @var string
+ */
+ private $_options;
+ /**
+ * @var array
+ */
+ private $_insertPositions = ['bottom', 'top'];
+ /**
+ * @var string the hashed global variable name storing the pluginOptions.
+ */
+ private $_hashVar;
+ /**
+ * @var string the Json encoded options.
+ */
+ private $_encodedOptions = '';
+
+ /**
+ * Initializes the widget.
+ *
+ * @throws \yii\base\InvalidConfigException
+ */
+ public function init()
+ {
+ parent::init();
+
+ if (empty($this->widgetContainer) || (preg_match('/^\w{1,}$/', $this->widgetContainer) === 0)) {
+ throw new InvalidConfigException('Invalid configuration to property "widgetContainer".
+ Allowed only alphanumeric characters plus underline: [A-Za-z0-9_]');
+ }
+ if (empty($this->widgetBody)) {
+ throw new InvalidConfigException("The 'widgetBody' property must be set.");
+ }
+ if (empty($this->widgetItem)) {
+ throw new InvalidConfigException("The 'widgetItem' property must be set.");
+ }
+ if (empty($this->model) || !$this->model instanceof \yii\base\Model) {
+ throw new InvalidConfigException("The 'model' property must be set and must extend from '\\yii\\base\\Model'.");
+ }
+ if (empty($this->formId)) {
+ throw new InvalidConfigException("The 'formId' property must be set.");
+ }
+ if (empty($this->insertPosition) || !in_array($this->insertPosition, $this->_insertPositions)) {
+ throw new InvalidConfigException("Invalid configuration to property 'insertPosition' (allowed values: 'bottom' or 'top')");
+ }
+ if (empty($this->formFields) || !is_array($this->formFields)) {
+ throw new InvalidConfigException("The 'formFields' property must be set.");
+ }
+
+ $this->initOptions();
+ }
+
+ /**
+ * Initializes the widget options.
+ */
+ protected function initOptions()
+ {
+ $this->_options['widgetContainer'] = $this->widgetContainer;
+ $this->_options['widgetBody'] = $this->widgetBody;
+ $this->_options['widgetItem'] = $this->widgetItem;
+ $this->_options['limit'] = $this->limit;
+ $this->_options['insertButton'] = $this->insertButton;
+ $this->_options['deleteButton'] = $this->deleteButton;
+ $this->_options['insertPosition'] = $this->insertPosition;
+ $this->_options['formId'] = $this->formId;
+ $this->_options['min'] = $this->min;
+ $this->_options['fields'] = [];
+
+ foreach ($this->formFields as $field) {
+ $this->_options['fields'][] = [
+ 'id' => Html::getInputId($this->model, '[{}]' . $field),
+ 'name' => Html::getInputName($this->model, '[{}]' . $field)
+ ];
+ }
+
+ ob_start();
+ ob_implicit_flush(false);
+ }
+
+ /**
+ * Registers plugin options by storing it in a hashed javascript variable.
+ *
+ * @param View $view The View object
+ */
+ protected function registerOptions($view)
+ {
+ $view->registerJs("var {$this->_hashVar} = {$this->_encodedOptions};\n", $view::POS_HEAD);
+ }
+
+ /**
+ * Generates a hashed variable to store the options.
+ */
+ protected function hashOptions()
+ {
+ $this->_encodedOptions = Json::encode($this->_options);
+ $this->_hashVar = self::WIDGET_NAME . '_' . hash('crc32', $this->_encodedOptions);
+ }
+
+ /**
+ * Returns the hashed variable.
+ *
+ * @return string
+ */
+ protected function getHashVarName()
+ {
+ if (isset(Yii::$app->params[self::WIDGET_NAME][$this->widgetContainer])) {
+ return Yii::$app->params[self::WIDGET_NAME][$this->widgetContainer];
+ }
+
+ return $this->_hashVar;
+ }
+
+ /**
+ * Register the actual widget.
+ *
+ * @return boolean
+ */
+ public function registerHashVarWidget()
+ {
+ if (!isset(Yii::$app->params[self::WIDGET_NAME][$this->widgetContainer])) {
+ Yii::$app->params[self::WIDGET_NAME][$this->widgetContainer] = $this->_hashVar;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Registers the needed assets.
+ *
+ * @param View $view The View object
+ */
+ public function registerAssets($view)
+ {
+ DynamicFormAsset::register($view);
+
+ // add a click handler for the clone button
+ $js = 'jQuery("#' . $this->formId . '").on("click", "' . $this->insertButton . '", function(e) {' . "\n";
+ $js .= " e.preventDefault();\n";
+ $js .= ' jQuery(".' . $this->widgetContainer . '").triggerHandler("beforeInsert", [jQuery(this)]);' . "\n";
+ $js .= ' jQuery(".' . $this->widgetContainer . '").yiiDynamicForm("addItem", ' . $this->_hashVar . ", e, jQuery(this));\n";
+ $js .= "});\n";
+ $view->registerJs($js, $view::POS_READY);
+
+ // add a click handler for the remove button
+ $js = 'jQuery("#' . $this->formId . '").on("click", "' . $this->deleteButton . '", function(e) {' . "\n";
+ $js .= " e.preventDefault();\n";
+ $js .= ' jQuery(".' . $this->widgetContainer . '").yiiDynamicForm("deleteItem", ' . $this->_hashVar . ", e, jQuery(this));\n";
+ $js .= "});\n";
+ $view->registerJs($js, $view::POS_READY);
+
+ $js = 'jQuery("#' . $this->formId . '").yiiDynamicForm(' . $this->_hashVar . ');' . "\n";
+ $view->registerJs($js, $view::POS_LOAD);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run()
+ {
+ $content = ob_get_clean();
+ $crawler = new Crawler();
+ $crawler->addHTMLContent($content, \Yii::$app->charset);
+ $results = $crawler->filter($this->widgetItem);
+ $document = new \DOMDocument('1.0', \Yii::$app->charset);
+ $document->appendChild($document->importNode($results->first()->getNode(0), true));
+ $this->_options['template'] = trim($document->saveHTML());
+
+ if (isset($this->_options['min']) && $this->_options['min'] === 0 && $this->model->isNewRecord) {
+ $content = $this->removeItems($content);
+ }
+
+ $this->hashOptions();
+ $view = $this->getView();
+ $widgetRegistered = $this->registerHashVarWidget();
+ $this->_hashVar = $this->getHashVarName();
+
+ if ($widgetRegistered) {
+ $this->registerOptions($view);
+ $this->registerAssets($view);
+ }
+
+ echo Html::tag('div', $content, ['class' => $this->widgetContainer, 'data-dynamicform' => $this->_hashVar]);
+ }
+
+ /**
+ * Clear HTML widgetBody. Required to work with zero or more items.
+ *
+ * @param string $content
+ */
+ private function removeItems($content)
+ {
+ $crawler = new Crawler();
+ $crawler->addHTMLContent($content, \Yii::$app->charset);
+ $crawler->filter($this->widgetItem)->each(function ($nodes) {
+ foreach ($nodes as $node) {
+ $node->parentNode->removeChild($node);
+ }
+ });
+
+ return $crawler->html();
+ }
+}
diff --git a/src/Models/Model.php b/src/Models/Model.php
new file mode 100644
index 0000000..953754c
--- /dev/null
+++ b/src/Models/Model.php
@@ -0,0 +1,45 @@
+formName();
+ $post = Yii::$app->request->post($formName);
+ $models = [];
+
+ if (!empty($multipleModels)) {
+ $keys = array_keys(ArrayHelper::map($multipleModels, 'id', 'id'));
+ $multipleModels = array_combine($keys, $multipleModels);
+ }
+
+ if ($post && is_array($post)) {
+ foreach ($post as $i => $item) {
+ if (isset($item['id']) && !empty($item['id']) && isset($multipleModels[$item['id']])) {
+ $models[] = $multipleModels[$item['id']];
+ } else {
+ $models[] = new $modelClass;
+ }
+ }
+ }
+
+ unset($model, $formName, $post);
+
+ return $models;
+ }
+}
diff --git a/src/assets/yii2-dynamic-form.js b/src/assets/yii2-dynamic-form.js
new file mode 100644
index 0000000..093b739
--- /dev/null
+++ b/src/assets/yii2-dynamic-form.js
@@ -0,0 +1,472 @@
+/**
+ * Yii2 Dynamic form
+ *
+ * A jQuery plugin to clone form elements in a nested manner, maintaining accessibility.
+ *
+ * @author Wanderson Bragança
+ * @author Stefano Mtangoo
+ */
+(function ($) {
+ var pluginName = 'yiiDynamicForm';
+
+ var regexID = /^(.+?)([-\d-]{1,})(.+)$/i;
+
+ var regexName = /(^.+?)([\[\d{1,}\]]{1,})(\[.+\]$)/i;
+
+ $.fn.yiiDynamicForm = function (method) {
+ if (methods[method]) {
+ return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
+ } else if (typeof method === 'object' || !method) {
+ return methods.init.apply(this, arguments);
+ } else {
+ $.error('Method ' + method + ' does not exist on jQuery.yiiDynamicForm');
+ return false;
+ }
+ };
+
+ var events = {
+ beforeInsert: 'beforeInsert',
+ afterInsert: 'afterInsert',
+ beforeDelete: 'beforeDelete',
+ afterDelete: 'afterDelete',
+ limitReached: 'limitReached'
+ };
+
+ var methods = {
+ init: function (widgetOptions) {
+ return this.each(function () {
+ widgetOptions.template = _parseTemplate(widgetOptions);
+ });
+ },
+
+ addItem: function (widgetOptions, e, $elem) {
+ _addItem(widgetOptions, e, $elem);
+ },
+
+ deleteItem: function (widgetOptions, e, $elem) {
+ _deleteItem(widgetOptions, e, $elem);
+ },
+
+ updateContainer: function () {
+ var widgetOptions = eval($(this).attr('data-dynamicform'));
+ _updateAttributes(widgetOptions);
+ _restoreSpecialJs(widgetOptions);
+ _fixFormValidaton(widgetOptions);
+ }
+ };
+
+ var _parseTemplate = function (widgetOptions) {
+
+ var $template = $(widgetOptions.template);
+ $template.find('div[data-dynamicform]').each(function () {
+ var widgetOptions = eval($(this).attr('data-dynamicform'));
+ if ($(widgetOptions.widgetItem).length > 1) {
+ var item = $(this).find(widgetOptions.widgetItem).first()[0].outerHTML;
+ $(this).find(widgetOptions.widgetBody).html(item);
+ }
+ });
+
+ $template.find('input, textarea, select').each(function () {
+ $(this).val('');
+ });
+
+ $template.find('input[type="checkbox"], input[type="radio"]').each(function () {
+ var inputName = $(this).attr('name');
+ var $inputHidden = $template.find('input[type="hidden"][name="' + inputName + '"]').first();
+ if ($inputHidden) {
+ $(this).val(1);
+ $inputHidden.val(0);
+ }
+ });
+
+ return $template;
+ };
+
+ var _getWidgetOptionsRoot = function (widgetOptions) {
+ return eval($(widgetOptions.widgetBody).parents('div[data-dynamicform]').last().attr('data-dynamicform'));
+ };
+
+ var _getLevel = function ($elem) {
+ var level = $elem.parents('div[data-dynamicform]').length;
+ level = (level < 0) ? 0 : level;
+ return level;
+ };
+
+ var _count = function ($elem, widgetOptions) {
+ return $elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetItem).length;
+ };
+
+ var _createIdentifiers = function (level) {
+ return new Array(level + 2).join('0').split('');
+ };
+
+ var _addItem = function (widgetOptions, e, $elem) {
+ var count = _count($elem, widgetOptions);
+
+ if (count < widgetOptions.limit) {
+ $toclone = $(widgetOptions.template);
+ $newclone = $toclone.clone(false, false);
+
+ if (widgetOptions.insertPosition === 'top') {
+ $elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetBody).prepend($newclone);
+ } else {
+ $elem.closest('.' + widgetOptions.widgetContainer).find(widgetOptions.widgetBody).append($newclone);
+ }
+
+ _updateAttributes(widgetOptions);
+ _restoreSpecialJs(widgetOptions);
+ _fixFormValidaton(widgetOptions);
+ $elem.closest('.' + widgetOptions.widgetContainer).triggerHandler(events.afterInsert, $newclone);
+ } else {
+ // trigger a custom event for hooking
+ $elem.closest('.' + widgetOptions.widgetContainer).triggerHandler(events.limitReached, widgetOptions.limit);
+ }
+ };
+
+ var _removeValidations = function ($elem, widgetOptions, count) {
+ if (count > 1) {
+ $elem.find('div[data-dynamicform]').each(function () {
+ var currentWidgetOptions = eval($(this).attr('data-dynamicform'));
+ var level = _getLevel($(this));
+ var identifiers = _createIdentifiers(level);
+ var numItems = $(this).find(currentWidgetOptions.widgetItem).length;
+
+ for (var i = 1; i <= numItems - 1; i++) {
+ var aux = identifiers;
+ aux[level] = i;
+ currentWidgetOptions.fields.forEach(function (input) {
+ var id = input.id.replace("{}", aux.join('-'));
+ if ($("#" + currentWidgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
+ $("#" + currentWidgetOptions.formId).yiiActiveForm("remove", id);
+ }
+ });
+ }
+ });
+
+ var level = _getLevel($elem.closest('.' + widgetOptions.widgetContainer));
+ var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
+ var identifiers = _createIdentifiers(level);
+ identifiers[0] = $(widgetOptionsRoot.widgetItem).length - 1;
+ identifiers[level] = count - 1;
+
+ widgetOptions.fields.forEach(function (input) {
+ var id = input.id.replace("{}", identifiers.join('-'));
+ if ($("#" + widgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
+ $("#" + widgetOptions.formId).yiiActiveForm("remove", id);
+ }
+ });
+ }
+ };
+
+ var _deleteItem = function (widgetOptions, e, $elem) {
+ var count = _count($elem, widgetOptions);
+
+ if (count > widgetOptions.min) {
+ $todelete = $elem.closest(widgetOptions.widgetItem);
+
+ // trigger a custom event for hooking
+ var eventResult = $('.' + widgetOptions.widgetContainer).triggerHandler(events.beforeDelete, $todelete);
+ if (eventResult !== false) {
+ _removeValidations($todelete, widgetOptions, count);
+ $todelete.remove();
+ _updateAttributes(widgetOptions);
+ _restoreSpecialJs(widgetOptions);
+ _fixFormValidaton(widgetOptions);
+ $('.' + widgetOptions.widgetContainer).triggerHandler(events.afterDelete);
+ }
+ }
+ };
+
+ var _updateAttrID = function ($elem, index) {
+ var widgetOptions = eval($elem.closest('div[data-dynamicform]').attr('data-dynamicform'));
+ var id = $elem.attr('id');
+ var newID = id;
+
+ if (id !== undefined) {
+ var matches = id.match(regexID);
+ if (matches && matches.length === 4) {
+ matches[2] = matches[2].substring(1, matches[2].length - 1);
+ var identifiers = matches[2].split('-');
+ identifiers[0] = index;
+
+ if (identifiers.length > 1) {
+ var widgetsOptions = [];
+ $elem.parents('div[data-dynamicform]').each(function (i) {
+ widgetsOptions[i] = eval($(this).attr('data-dynamicform'));
+ });
+
+ widgetsOptions = widgetsOptions.reverse();
+ for (var i = identifiers.length - 1; i >= 1; i--) {
+ if (widgetsOptions[i]) {
+ identifiers[i] = $elem.closest(widgetsOptions[i].widgetItem).index();
+ }
+ }
+ }
+
+ newID = matches[1] + '-' + identifiers.join('-') + '-' + matches[3];
+ $elem.attr('id', newID);
+ } else {
+ newID = id + index;
+ $elem.attr('id', newID);
+ }
+ }
+
+ if (id !== newID) {
+ $elem.closest(widgetOptions.widgetItem).find('.field-' + id).each(function () {
+ $(this).removeClass('field-' + id).addClass('field-' + newID);
+ });
+ // update "for" attribute
+ $elem.closest(widgetOptions.widgetItem).find("label[for='" + id + "']").attr('for', newID);
+ }
+
+ return newID;
+ };
+
+ var _updateAttrName = function ($elem, index) {
+ var name = $elem.attr('name');
+
+ if (name !== undefined) {
+ var matches = name.match(regexName);
+
+ if (matches && matches.length === 4) {
+ matches[2] = matches[2].replace(/\]\[/g, "-").replace(/\]|\[/g, '');
+ var identifiers = matches[2].split('-');
+ identifiers[0] = index;
+
+ if (identifiers.length > 1) {
+ var widgetsOptions = [];
+ $elem.parents('div[data-dynamicform]').each(function (i) {
+ widgetsOptions[i] = eval($(this).attr('data-dynamicform'));
+ });
+
+ widgetsOptions = widgetsOptions.reverse();
+ for (var i = identifiers.length - 1; i >= 1; i--) {
+ identifiers[i] = $elem.closest(widgetsOptions[i].widgetItem).index();
+ }
+ }
+
+ name = matches[1] + '[' + identifiers.join('][') + ']' + matches[3];
+ $elem.attr('name', name);
+ }
+ }
+
+ return name;
+ };
+
+ var _updateAttributes = function (widgetOptions) {
+ var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
+
+ $(widgetOptionsRoot.widgetItem).each(function (index) {
+ var $item = $(this);
+ $(this).find('*').each(function () {
+ // update "id" attribute
+ _updateAttrID($(this), index);
+
+ // update "name" attribute
+ _updateAttrName($(this), index);
+ });
+ });
+ };
+
+ var _fixFormValidatonInput = function (widgetOptions, attribute, id, name) {
+ if (attribute !== undefined) {
+ attribute = $.extend(true, {}, attribute);
+ attribute.id = id;
+ attribute.container = ".field-" + id;
+ attribute.input = "#" + id;
+ attribute.name = name;
+ attribute.value = $("#" + id).val();
+ attribute.status = 0;
+
+ if ($("#" + widgetOptions.formId).yiiActiveForm("find", id) !== "undefined") {
+ $("#" + widgetOptions.formId).yiiActiveForm("remove", id);
+ }
+
+ $("#" + widgetOptions.formId).yiiActiveForm("add", attribute);
+ }
+ };
+
+ var _fixFormValidaton = function (widgetOptions) {
+ var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
+
+ $(widgetOptionsRoot.widgetBody).find('input, textarea, select').each(function () {
+ var id = $(this).attr('id');
+ var name = $(this).attr('name');
+
+ if (id !== undefined && name !== undefined) {
+ currentWidgetOptions = eval($(this).closest('div[data-dynamicform]').attr('data-dynamicform'));
+ var matches = id.match(regexID);
+
+ if (matches && matches.length === 4) {
+ matches[2] = matches[2].substring(1, matches[2].length - 1);
+ var level = _getLevel($(this));
+ var identifiers = _createIdentifiers(level - 1);
+ var baseID = matches[1] + '-' + identifiers.join('-') + '-' + matches[3];
+ var attribute = $("#" + currentWidgetOptions.formId).yiiActiveForm("find", baseID);
+ _fixFormValidatonInput(currentWidgetOptions, attribute, id, name);
+ }
+ }
+ });
+ };
+
+ var _restoreKrajeeDepdrop = function ($elem) {
+ var configDepdrop = $.extend(true, {}, eval($elem.attr('data-krajee-depdrop')));
+ var inputID = $elem.attr('id');
+ var matchID = inputID.match(regexID);
+
+ if (matchID && matchID.length === 4) {
+ for (index = 0; index < configDepdrop.depends.length; ++index) {
+ var match = configDepdrop.depends[index].match(regexID);
+ if (match && match.length === 4) {
+ configDepdrop.depends[index] = match[1] + matchID[2] + match[3];
+ }
+ }
+ }
+ $elem.depdrop(configDepdrop);
+ };
+
+ var _restoreSpecialJs = function (widgetOptions) {
+ var widgetOptionsRoot = _getWidgetOptionsRoot(widgetOptions);
+
+ // "jquery.inputmask"
+ var $hasInputmask = $(widgetOptionsRoot.widgetItem).find('[data-plugin-inputmask]');
+ if ($hasInputmask.length > 0) {
+ $hasInputmask.each(function () {
+ $(this).inputmask('remove');
+ $(this).inputmask(eval($(this).attr('data-plugin-inputmask')));
+ });
+ }
+
+ // JUI Datepicker
+ /*$( ".picker" ).each(function() {
+ $( this ).datepicker({
+ dateFormat : 'dd-mm-yy',
+ language : 'en',
+ });
+ });*/
+
+ // "kartik-v/yii2-widget-datepicker"
+ var datePickers = $(widgetOptionsRoot.widgetItem).find('[data-krajee-kvdatepicker]');
+ datePickers.each(function (index, el) {
+ //$(this).parent().removeData().kvDatepicker('remove');
+ $(this).parent().kvDatepicker(eval($(this).attr('data-krajee-kvdatepicker')));
+ });
+
+ // "kartik-v/yii2-widget-timepicker"
+ var $hasTimepicker = $(widgetOptionsRoot.widgetItem).find('[data-krajee-timepicker]');
+ if ($hasTimepicker.length > 0) {
+ $hasTimepicker.each(function () {
+ $(this).removeData().off();
+ $(this).parent().find('.bootstrap-timepicker-widget').remove();
+ $(this).unbind();
+ $(this).timepicker(eval($(this).attr('data-krajee-timepicker')));
+ });
+ }
+
+ // "kartik-v/yii2-money"
+ var $hasMaskmoney = $(widgetOptionsRoot.widgetItem).find('[data-krajee-maskMoney]');
+ if ($hasMaskmoney.length > 0) {
+ $hasMaskmoney.each(function () {
+ $(this).parent().find('input').removeData().off();
+ var id = '#' + $(this).attr('id');
+ var displayID = id + '-disp';
+ $(displayID).maskMoney('destroy');
+ $(displayID).maskMoney(eval($(this).attr('data-krajee-maskMoney')));
+ $(displayID).maskMoney('mask', parseFloat($(id).val()));
+ $(displayID).on('change', function () {
+ var numDecimal = $(displayID).maskMoney('unmasked')[0];
+ $(id).val(numDecimal);
+ $(id).trigger('change');
+ });
+ });
+ }
+
+ // "kartik-v/yii2-widget-fileinput"
+ var $hasFileinput = $(widgetOptionsRoot.widgetItem).find('[data-krajee-fileinput]');
+ if ($hasFileinput.length > 0) {
+ $hasFileinput.each(function () {
+ $(this).fileinput(eval($(this).attr('data-krajee-fileinput')));
+ });
+ }
+
+ // "kartik-v/yii2-widget-touchspin"
+ var $hasTouchSpin = $(widgetOptionsRoot.widgetItem).find('[data-krajee-TouchSpin]');
+ if ($hasTouchSpin.length > 0) {
+ $hasTouchSpin.each(function () {
+ $(this).TouchSpin('destroy');
+ $(this).TouchSpin(eval($(this).attr('data-krajee-TouchSpin')));
+ });
+ }
+
+ // "kartik-v/yii2-widget-colorinput"
+ var $hasSpectrum = $(widgetOptionsRoot.widgetItem).find('[data-krajee-spectrum]');
+ if ($hasSpectrum.length > 0) {
+ $hasSpectrum.each(function () {
+ var id = '#' + $(this).attr('id');
+ var sourceID = id + '-source';
+ $(sourceID).spectrum('destroy');
+ $(sourceID).unbind();
+ $(id).unbind();
+ var configSpectrum = eval($(this).attr('data-krajee-spectrum'));
+ configSpectrum.change = function (color) {
+ jQuery(id).val(color.toString());
+ };
+ $(sourceID).attr('name', $(sourceID).attr('id'));
+ $(sourceID).spectrum(configSpectrum);
+ $(sourceID).spectrum('set', jQuery(id).val());
+ $(id).on('change', function () {
+ $(sourceID).spectrum('set', jQuery(id).val());
+ });
+ });
+ }
+
+ // "kartik-v/yii2-widget-depdrop"
+ var $hasDepdrop = $(widgetOptionsRoot.widgetItem).find('[data-krajee-depdrop]');
+ if ($hasDepdrop.length > 0) {
+ $hasDepdrop.each(function () {
+ if ($(this).data('select2') === undefined) {
+ $(this).removeData().off();
+ $(this).unbind();
+ _restoreKrajeeDepdrop($(this));
+ }
+ var configDepdrop = eval($(this).attr('data-krajee-depdrop'));
+ $(this).depdrop(configDepdrop);
+ });
+ }
+
+ // "kartik-v/yii2-widget-select2"
+ var $hasSelect2 = $(widgetOptionsRoot.widgetItem).find('[data-krajee-select2]');
+ if ($hasSelect2.length > 0) {
+ $hasSelect2.each(function () {
+ var id = $(this).attr('id');
+ var configSelect2 = eval($(this).attr('data-krajee-select2'));
+ $.when($('#' + id).select2(configSelect2)).done(initS2Loading(id));
+ $('#' + id).on('select2-open', function () {
+ initSelect2DropStyle(id)
+ });
+ if ($(this).attr('data-krajee-depdrop')) {
+ $(this).on('depdrop.beforeChange', function (e, i, v) {
+ var configDepdrop = eval($(this).attr('data-krajee-depdrop'));
+ var loadingText = (configDepdrop.loadingText) ? configDepdrop.loadingText : 'Loading ...';
+ $('#' + id).select2('data', { text: loadingText });
+ });
+ $(this).on('depdrop.change', function (e, i, v, c) {
+ $('#' + id).select2('val', $('#' + id).val());
+ });
+ }
+ });
+ }
+
+ // "kartik-v/yii2-numbercontrol"
+ var $hasNumberControl = $(widgetOptionsRoot.widgetItem).find('[data-krajee-numbercontrol]');
+ if ($hasNumberControl.length > 0) {
+ $hasNumberControl.each(function () {
+ var configNumberControl = eval($(this).attr('data-krajee-numbercontrol'));
+ configNumberControl.displayId = $(this).parent().prev().attr('id');
+ if ($(this).data('numberControl')) { $(this).numberControl('destroy'); }
+ $(this).numberControl(configNumberControl);
+ });
+ }
+ };
+
+})(window.jQuery);
diff --git a/src/assets/yii2-dynamic-form.min.js b/src/assets/yii2-dynamic-form.min.js
new file mode 100644
index 0000000..fb505a8
--- /dev/null
+++ b/src/assets/yii2-dynamic-form.min.js
@@ -0,0 +1 @@
+!function($){var pluginName="yiiDynamicForm",regexID=/^(.+?)([-\d-]{1,})(.+)$/i,regexName=/(^.+?)([\[\d{1,}\]]{1,})(\[.+\]$)/i,events=($.fn.yiiDynamicForm=function(method){return methods[method]?methods[method].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof method&&method?($.error("Method "+method+" does not exist on jQuery.yiiDynamicForm"),!1):methods.init.apply(this,arguments)},{beforeInsert:"beforeInsert",afterInsert:"afterInsert",beforeDelete:"beforeDelete",afterDelete:"afterDelete",limitReached:"limitReached"}),methods={init:function(widgetOptions){return this.each(function(){widgetOptions.template=_parseTemplate(widgetOptions)})},addItem:function(widgetOptions,e,$elem){_addItem(widgetOptions,e,$elem)},deleteItem:function(widgetOptions,e,$elem){_deleteItem(widgetOptions,e,$elem)},updateContainer:function(){var widgetOptions=eval($(this).attr("data-dynamicform"));_updateAttributes(widgetOptions),_restoreSpecialJs(widgetOptions),_fixFormValidaton(widgetOptions)}},_parseTemplate=function(widgetOptions){var $template=$(widgetOptions.template);return $template.find("div[data-dynamicform]").each(function(){var widgetOptions=eval($(this).attr("data-dynamicform")),item;1<$(widgetOptions.widgetItem).length&&(item=$(this).find(widgetOptions.widgetItem).first()[0].outerHTML,$(this).find(widgetOptions.widgetBody).html(item))}),$template.find("input, textarea, select").each(function(){$(this).val("")}),$template.find('input[type="checkbox"], input[type="radio"]').each(function(){var inputName=$(this).attr("name"),inputName=$template.find('input[type="hidden"][name="'+inputName+'"]').first();inputName&&($(this).val(1),inputName.val(0))}),$template},_getWidgetOptionsRoot=function(widgetOptions){return eval($(widgetOptions.widgetBody).parents("div[data-dynamicform]").last().attr("data-dynamicform"))},_getLevel=function($elem){$elem=$elem.parents("div[data-dynamicform]").length;return $elem<0?0:$elem},_count=function($elem,widgetOptions){return $elem.closest("."+widgetOptions.widgetContainer).find(widgetOptions.widgetItem).length},_createIdentifiers=function(level){return new Array(level+2).join("0").split("")},_addItem=function(widgetOptions,e,$elem){_count($elem,widgetOptions)widgetOptions.min&&($todelete=$elem.closest(widgetOptions.widgetItem),!1!==$("."+widgetOptions.widgetContainer).triggerHandler(events.beforeDelete,$todelete))&&(_removeValidations($todelete,widgetOptions,count),$todelete.remove(),_updateAttributes(widgetOptions),_restoreSpecialJs(widgetOptions),_fixFormValidaton(widgetOptions),$("."+widgetOptions.widgetContainer).triggerHandler(events.afterDelete))},_updateAttrID=function($elem,index){var widgetOptions=eval($elem.closest("div[data-dynamicform]").attr("data-dynamicform")),id=$elem.attr("id"),newID=id;if(void 0!==id){var matches=id.match(regexID);if(matches&&4===matches.length){matches[2]=matches[2].substring(1,matches[2].length-1);var identifiers=matches[2].split("-");if(identifiers[0]=index,1 'testApp',
+ 'basePath' => __DIR__
+]);