From baf3fff05ed5431e8e8a12c06307061d0ae60bfa Mon Sep 17 00:00:00 2001 From: Stefano Mtangoo Date: Wed, 31 Jan 2024 18:53:45 +0300 Subject: [PATCH] First version of the Extension --- .gitattributes | 10 + .gitignore | 27 ++ .scrutinizer.yml | 35 +++ .travis.yml | 24 ++ CHANGELOG.md | 47 +++ LICENSE.md | 28 ++ README.md | 366 +++++++++++++++++++++ composer.json | 49 +++ images/scenario.jpeg | Bin 0 -> 57817 bytes phpunit.xml.dist | 29 ++ src/DynamicFormAsset.php | 65 ++++ src/DynamicFormWidget.php | 272 ++++++++++++++++ src/Models/Model.php | 45 +++ src/assets/yii2-dynamic-form.js | 472 ++++++++++++++++++++++++++++ src/assets/yii2-dynamic-form.min.js | 1 + tests/bootstrap.php | 14 + 16 files changed, 1484 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 images/scenario.jpeg create mode 100644 phpunit.xml.dist create mode 100644 src/DynamicFormAsset.php create mode 100644 src/DynamicFormWidget.php create mode 100644 src/Models/Model.php create mode 100644 src/assets/yii2-dynamic-form.js create mode 100644 src/assets/yii2-dynamic-form.min.js create mode 100644 tests/bootstrap.php 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 + + +
+ 'dynamic-form']); ?> +
+
+ field($modelCustomer, 'first_name')->textInput(['maxlength' => true]) ?> +
+
+ field($modelCustomer, 'last_name')->textInput(['maxlength' => true]) ?> +
+
+ +
+

Addresses

+
+ 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_] + 'widgetBody' => '.container-items', // required: css class selector + 'widgetItem' => '.item', // required: css class. + 'limit' => 4, // the maximum times, an element can be cloned (default 999) + 'min' => 1, // 0 or 1 (default 1) + 'insertButton' => '.add-item', // css class + 'deleteButton' => '.remove-item', // css class + 'model' => $modelsAddress[0], + 'formId' => 'dynamic-form', + 'formFields' => [ + 'full_name', + 'address_line1', + 'address_line2', + 'city', + 'state', + 'postal_code', + ], + ]); ?> + +
+ $modelAddress): ?> +
+
+

Address

+
+ + +
+
+
+
+ isNewRecord) { + echo Html::activeHiddenInput($modelAddress, "[{$i}]id"); + } + ?> + field($modelAddress, "[{$i}]full_name")->textInput(['maxlength' => true]) ?> +
+
+ field($modelAddress, "[{$i}]address_line1")->textInput(['maxlength' => true]) ?> +
+
+ field($modelAddress, "[{$i}]address_line2")->textInput(['maxlength' => true]) ?> +
+
+
+
+ field($modelAddress, "[{$i}]city")->textInput(['maxlength' => true]) ?> +
+
+ field($modelAddress, "[{$i}]state")->textInput(['maxlength' => true]) ?> +
+
+ field($modelAddress, "[{$i}]postal_code")->textInput(['maxlength' => true]) ?> +
+
+
+
+ +
+ +
+
+ +
+ isNewRecord ? 'Create' : 'Update', ['class' => 'btn btn-primary']) ?> +
+ + + +
+``` + +### 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 0000000000000000000000000000000000000000..88f10164f607a0af36a207e04aba2734d571b045 GIT binary patch literal 57817 zcmeFZby!^AvLM=62n2U0NaOBKa2mJ9-Q6`n2oM|^cMVQsp>a)uJHb5!x8N51@jLU) zId|@xJMY{#ciznRy}R~5y?gClwW`*tT4ifLFFgMOU@6FgWC3t+Z~*z23-G)QkoxQH zf2#cj~lZU%TfWrpBW5Xd}!#(!`C;)H(czA@De!zci$gh!J!NDV< zAfUeV8(;wdNQeM9Boq`BG!#U51ONge(ko;D9Qt-gnX9~UR>O;!{?lK`U$FiK@n0ZE zL`L`v`)icHkXvH|;1J;95MQI>p}s~!KtKY#;QbqZY#dxX0xAu#d(sp>5j7_lw}fZn z93c-cpSq@&j#qMOS{G(+o%9qkMl6aOAZER~EQ&tWM#||^%O6!=Zd4kmpe|0a zD(5$tg87>W?q6?(YDvedY}c>tvRd4LR-9BjG96aX;xFNCvC5`aS~41z-@cIj8aUq! za>)Pl1H;wg*6nNK#Dgue0Ov!?+S2Y5&nrgqo{)VBNK)GZ;3KpxF7FM);n=hf->QL{ zOF0^<|Hlxs3lv$*QetuxPxx+XA#6ld4?ZpYW&3@FvUi_K<4R&pWtuFk?o-;zw}MJU z2=$Nt#G5n`nh}gqbHvq8uU0aF)bK2T`OZMv{Gj?FBZ)zHkW~1*B7q9g!uPCi06OpK z0%Qxfu6@Nr6s-X!YRzDQ%G8cstCkULJ!<%GiFRCJeAMqPP z`WqVVnVK?i2>jz{L2UfjttZ@U-&zMZlR1UN^>HoW-3LIso8^O{GmAX@lzgL|3oMtU!a)t_vyjzp)<3Q|BlTw zqGMqAw!m9i2}zfrXAt^UA>sa>q3yXgpmCj_yW}%tj)BN8 zbPg^QwAmfS;Yd zXcRX2w|p6kYF6lT6tNkg)N?YvFca)yS#{$pD3>6e0%fjqxi@~q|% zC7Yn>x8Z+KZJoVvF&h#VXU$N6GA`U9e7bOB^j*Do;6~R5qP?Ccum-z6cl~PR(E@`O zMt?WaRAl~@CmK_knhkNzb>eXzyVKp}4dSYZw$kq3mKkl&{`yl=<=|X38)ZKt(7Gb7 zVS|F8a+^Qv-Q3VupHE(X^kbhcd%EhC|Lgy}FqhSJa%0einJC9e)wGj9L>Tl|c_+>> z`^HyJCn;b-RY~}eXIX`6-M&lHugHMz@h5H!zXq#=eqViKwKG9uTyA5vgCDDd2V_J) z+pbrHRcF|R$H8)~z1w4UE1wT{8slz^Z#vRfzc%Hk?SI@Lu>W@e-x!;T(VwexJaRAy ztG2Ll`Y$5737acja>INBKF=N_S>!)H&AIxIVDtYL3jd7-B2e!|J=a)eApAGow79H4 z*PJxhEbzOrqA5qJdp4l-LbgfyzJp%$-?fnbPonVO5CIjIRr#0oO)x&!*rGqj=RMb0 zz13JfF3f80REBlhXu{YrS)Kgh1ZCpi5S0=4&$Q)fW5P5@?nH^drMq5@nY0gJPuIrP zuEwRdv->{-=GsS5d|C~mhl@sfyoxlXalaj*(Q)o0nP6^eAAS^`6OWuDdZuHaAdt-U zYR*9-ud4OtHPhR1oAzDfop2AqCc|?fy$FPgF)~4HgTarCEn0~QxZz+OA+?Wk>G)HL z!`7m)j1}0lMExQ4AS?QG3*_;+?1U0fG@q~CXb`2n(iv_uWcXO#X^|H9!yx8seIpuC z>j+fwo$^IsXD#Z|`G}`1R3VA+#=K`p?{;L5=)dX}1q)X7xygf@}7_UY|Ww z3QGy=aE>Yyx+~Zps#i0qj0iHcK?bgUD)m2d3 zXUKpL&PYw5{iZ zrkQA(2LIRsNQqGX)@WxhQo@PPsw@*b9GsIC6*;Dazt%SiD#eAjm!0!L#XCqi%4ztd z47HDb%mP#68)VOw73kOOdYwRqN6Ub}Nx4&sN70*TD~aWQ2xrJCvAyZmVvl`?>o&El zpd;y6N(R5Kde>|0zmUN=x=Wy=7@vXFG(^SJLMufE6SF&7|1n1PGY;1RUu#_Ff6*yZ~to#*2PojuU zVEsrx!hB(@jPo!#cz{KImoEdUHIzVUX&S6((^ULQ>fvb=lL^E=B7!+6MH@`ee#@zZ zUWrA6U0+k}es1YA^vH*$HW+d!!P^_(dCa$ymS`bwlQjT0D=LWAl$K+DuFYJ&n$?HHS62$`{ z-_Ch|`^E<>=|mmjcg(8>9YpW1Iw#ZJ;sxD_9@o5)hxg)>KfQ90OP+z=zc=$Bv4Y83 zJJC?9FWthd<4A350+~|^)8BLP7y-vld@Do>rVvtD({#QgSiAuiFp*Gyq@ts+&jCO| ziGu0UK~*@GCnzL8)mQJWH2`*88c7@VOc!EJ_ThQXRW^jexpw!jE+VN181;y6?BpCU zi`iLd8%Nd|N>lBM1u^+2=M4e)t>U}gRa0>*jo#yBib?!hplO?a_7*Sam1K4wwiVTB zZh;e-&SKi}T4RjZ5>bDNq4^3i0__z+2c(V5B{e&kmFQgB&6lygtK z^5~YP3Qre#De0lIuqURGxA|CT{JtsDF1p^^FlI$07qTVL@_NK%U6!_vU`WIAXQI=x zT_YH1tpuxPf_ZsMdG)(HQ0us}Q2X38B-1Adyi%u^+OqcWTA8x2OZF9V{kR zAleYw38Umw4`;LI@npn_RVfin{g`l_ ztc9k`k%NgZ`;Z59y`;-CchEElT}*fe03|<#5)MVn8lWUjhxe80C@of3WiV2d(W@uSlifls4nknZquywb>METvI!jsBU!}+2>gh=!PdBZrF$y2sa z+(wys^(7UJH5~9r)Ii>;w0=EVPcli|Zm4Ej-zA1}z5$r0X1MEIO22JUwk>{fe=6tM zF88u+ig%(dtDG90d2pfOvf(K^b6ca|wdwHS>_@@T>@l?3@z-(WlNf&NTgMoTCeQ-} z+XmJpV4oAj3m~r2^-kI7duuaUCj}l>wjrUCBP7jKD(V#xNLvv#l4U*I>AQzi)=x^2 z@fRt`3j<|HESN;iA}U{Lxn??AGY?B9Poo7-W2g>1fROvjPmf`+ZZ9EE;`}#9S(5G) zU7}>}mXZ2$vjD^OmSj4cci23OoV?VZ9+N4Ey_KUeUni3P#Ch*gzLCu7P^kjoCkOs0 zqY;yZs3@sPCNeX=`#W4o~>LCi0nhHd#EHjX+_nrvIfy`Ci^K+kSF!DM;{Joq|ETb8er zyvu!0b@?DA6fwCow^62J;uYDDOC|mQe4IKIVv2Fza3@D6Xf z2os7w1KvCXelw!+l2<$fR{snt&A!W;e8+$UD8PgNsD=vn&-{c7*6P0{r*W`J2y!ns zfe>`xJmCx1-;8oMV;*cSZl9mQ+?}=$)8EUS_d<<_R!oXxhMUtcJ`xx9-v5HXG3pQa zRYN}Sk)~^PpZID(*mZ<-xtY{YU>VlFRHbKHSe}+v`c+2+Q(IvyM*oqJ-a|pihBHXf zu;O-zna36yeKd1;EPNnJu4Qt8IT!W5Pj0!1XkGl*F{P)7V`TF)pzWmGOFd{*TpxId z_xCPRYSw4Kar?pF-{Lz87f}5p9I^UpFed1}<}LzE_|S7zXk4e=5m3anmJldvLX*w@he0m0iwgmq8PelL!WL=S;Q`4zpcmLQj*^I{kY&FwV6|K5^ zJ;L9uNy^IMl*qxYvD!Zb{eCR~>-{Ey`M^)(0}?_ZVJ+uXqdBq8--i1v<*hkO-mTa_ z-fhW%rSm-J4hlI?LQ*WUW z)_!YmTu&<2(7)g|4=wiG-3vSBWc$6}{+Sq1$CM89A>0>g?EJ3!F!|IV>fTZ|fxg_e zcBXHZ-XtDd`duG^=-;6X{yu>Ju?``^S2_xw;_rlgU4arj_7g`1t!s7x&W0a^MLGn6 z=4LIomPh8XI!y3km9`BRp z2ki{2qxen6FWMjb|Hp*?>mk6E3}W^QEJ6#8T+IsvC3RQ~VeS&baIvQ-J%xD9f_1p4=?1y33STu9I2?`(imRnGDxYoClus*-$SQ9sS2B45(I@TmX z`XbmDO0$rMaBHT1GIETG>en4h=Y>NQ$*(Id8tzZ*N*5TZOWuKuehH9x~lE* z|9C>>9}id*y4__y1FGm(2VyM~cVgo+<~ys)l+lytHV#j&lY2QQ1s;`NoH*R$&3-QV z3t72Qho5uPrQt(|ZeBO{pbetEoDZczhbT90o5Zw!MKn9SCk}SgQkGXOZTFAGXK0=( z;+o=DpYB`EVv|HB6E8?^8XNXM!``nL1wKvrh6~YCR^;Dx2d?<`gD7kVZ{z%zmOOtf ziaN7(Jp=4Sq{wmOXC@xa#EwNQXybV-1Nu9UoP=y0^O^}wf*SIuAW;JcHcwyjeJD7S z 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 z zuIEMeylG20VW^f>(s$0nUCx@ywpdyYU@4zh!mxg}m4VR7SKa$-PLSMz1Z)>2hDhR3V%xXSJwFYoF{B z;)DF)W!fn>GOxOIH0<6!)yjmZk=brWFo;ZID{obE=n%w=NA;hAqz9kBqy$@|e zAeTVV;q>G1q-3k)5bVl1M^q$t?c&VcCoLA3TQ%3!!+Aouxjui^9LVP{O4Wp_Wov8R z->eX%b4 z$uGf*!uQ!dH|u5RV;|My7jp*}9z#VV=;Q=){=#-D|CH=hhwBnpf zNP>D@#++Kn=6C4F??8f&4=Y^O#?Ll?3bp6NkKKPhv5GbqE?Ga=fAf~G#3`DX=(9Xe zGuj81on164(hzFP{Do78DThaHUyz-S<$ex|k6)yiFQ4Z zr8{^~~B6>LuyHX3vwyEN0nQJJI6|hU&W~g z6c8C}6m&m$!2<}XA9fhBSk&j58xov)zE5=a;my35p7^Q__!~3)7b3v_|Aq*V*WS); zx>1i__3m;p6vya*gP1!^iUtYr9EOBIiM!{briEUx+oY=xdQW4^8L83LTaTvmH9W?)_70O-*)}j z3=XVy&HLBh{=WbXuZRC-^aemZ{Q3_cV)^(*{XZshnv?&9k!$}MXk&7IGf5Y!MfS6~ zE$_ia#Aq7D6bWXQ_?Q7JhUPZIhP=}?%rtm2yr-QWMvdGhvfbe@$q4lvY($14&)iuK zjI4TFvg*`|7vLH_#tP-X@ik2^CS7d;Mk5@fw+~49QQugOnRehO3bX)7cNpa{@7OYgk<0$oK#!i$KTzBx-Kf=bS|2=2?Okq+VGYUAhJyp|bIDTQDBpTzLtL|ADdp^PGojU#e?Br4^pTkly zR}U~#Tk)<%c2n23zw48~PcYo{cF|)L*KDC*_;sCvWPmOFi2YPE&eb?-NIH@HNo`Kk z>2B%%N>7_f$%YP|7b?7r1RQ#@z%v0fT$%B&NNSA7s{B249si<|13|zEC9shVm;?C_q?mDza;(w(6-jG z(uJtfRw(d#DWh#+ebt}!nOKQ_OHX{>8>)vrQpoPx#6_C1YIubX21M@(i}W<{nf{re zC9G+-LBU+pQM(=q^4X;#%_MlT5>ALmK|s4ygmRNCnt-Kw8B7(VCtqStWn>U8o&SuW zwl1dV#42T|#w;%5Fdc5PGQ1mdFS2kGcydvHT7lS9e(9Of?UkV!OIrOoD8}@&y^x_U zoi_*!vr380SK-En*IN0WH6y|2{)5!;XV^IT#qdY#V{d4WhKQ1ZcV>M1u#Sk(QHKhF z(~!}@2H{2MaH)OaIm@iiF{J6|3Shwy%B$xQOIyGiL2PogDLfHI}spn#yHtcr`kfhb+KypzoD$tr&x`cpJ?!D{kxO z?KK||*67f$bJ;MzTHmm0FxMgClWbW&5svL3?XIt~)nQ$>cF;S~M}BFD^*YkoSHK*L z#>QeZQA9zN8xx0TJ({4t5PqT&pZC^((X{7x6>{H3e%Z94#jSJ1@$68F&79;0eC8T9 zLJ4MBWHI*WkNK;%w$kUqZs}9!rZTH$7gZ-SG|ypB!UK?@l~{%D03)=f;p(EW6kSYx z1OJ^M7h6i*?ArDZX?>-#UV>k^6I(HzUZksKj^hG~=2oTjRhJ)v$CiCjexT1aMtDrS za9<{Psur4JGn;kEFrFE>OMjBhemM426l){TM^W0mLg6)2`pH*`2kjMmB0><(htlq5 z%&4BRxt-+XiBKpwv#N6QW8qJqSR2K=0k-hoA20jBA8T*_-cwprGWfU(TGJ67xPv_zT zl4Dd2BCQc}G=|mV*E#Q^wQV-!I}I6xat<%Ze4;X%QQsD9|oVoy~H%@6)Nj4k>$WlusRD#*$Nbaikab#;^r|5-~|C1NuIy4Zl;n< zmG_D&VQe9~<>-&ztx1!Dm)1}TvZqm%k!@D*j!A3%B@uYMZuD2fP`OQ;M+!zV3YfkV z2&;tnws%pADDi^MFC7Z%mF{^3y&RYaIpU4B7Iru@&uv-2$uzZ%ZKS?db60P<-H-{3 zmnf#;OeyUOcIUdD6<`^1ZT1**fC0&Cb?KvRAtY}C&Fq@&lvRI4{MRMo@1rV%J_ z*rK;}W$m!3RQmJ9x^RQi^cbJuD_;{{xfZSvT^hCS z#usXuHlh~*E|8(H|2Osgi&l6xo%DOXf3gDv4CM_&ZNI9{atV=jnBFLpG&{`KT?|1> zH)DSu<1Kj8URO~D?S)zIHrm!xld~~H)inzq4t{A>1EGI&))H;->N8p0fPP`_G#LLb zzR*yg=J;|3W+M*~&%2fNaE;p}cPEl{ZL@KFTt9-WQq^9|^9K>ojdT<*6CvZOio(eg z;JK1e7fal&K(x*u4#gv(k#nv}NCyI|7eg#rS@+i7L(Sgg?fb<(%?AUr>v_kD8?PCj zieRjCJRp@}#G#hfGj!9lw08?NU3$}Mrze6LSi<2;*A^=CC8#X&1F>2;ibj*_WvpS# zx5qC-4b3McU&MTcM8$i5zp1LQ+*4G;gC%{}G;y|srf3_U`%|G3pI?xxw~~sR>crYY z4@a96ADu7Mqen2 zg7-d1ptwVX)dlu5Nl`I`UWwg5-D;?~k(^i`Ln*CnY?xzx8ppZY8fX^xnu}m(B*reL)#tt~iI-at0zl7nz%+sJmr z^3ZK-*4bJf*6I4gv<`Z!E!3&nTlVmI|8!-BJq=w=rks6eQI+VpmRCnn2&-ZPab7l6 z9YX0mwg4rP?)I6R15BsWu``3Q1Leap zAE7jn3;>ZfP8x}HV@v@Mzc)+$@R8sRQeW@3ZC8Nj26Ja2$cXWG!04-{UY$KL#74#Q zJ{HZ*gN#YRsaw^BP`XVJlqP&L+dOIZwiQcpayB?(lObj? z?&}Kk*@OiuyST4d$mEOnYE)LLY09l|B|0TorM8+^;19RE#HOnEwG6FY0Ie1$CXb5d~=5yoK4hUJ)b5(abtrft{IBZmp~*b4kiD@7%D zkjJI@nP^vnQr?N27vHq~q=|D$y{Puv&}Hghc`SEn-P^gg*tHwq>CZ)-BvSVUtaRH& zIBYC-@%8(_Ysv@~(Nw3R4e3n$`HlSeW7OC7xbso!{>EwZkLj{_lmgggp*x5Z!>e5# zD&*H`JCfFb51hvZ_N*8mHk2`)D!n)wHnTQAo3F+x zq&mamx>c7=wjH6h3y3SL|C%n zn^HMy1}#T1ctzQLi1$u-12hEfYv{uqo-~bZf~&3Q#&tq8XtYuevkp|*#ef!s0##^hM#mHYmxS_j$_2i7sTwv8O-RQgLC<2|uKBsLHyaU5_N zm)C1%d6MYk<`N6*4G@qf$$4#;rD>O`)G{E3EN9^DO*5$C>T?J%nK7$UN2Rl} zE3tQmm0A)dg7VJJ*_4l~Ql-fSnHL?P>Dgw!A)_^|!jnXUc(Yoj-QrO+Uz&R1EJer3 znHl8Q`mU0fM_*a4Fmh3+|fW?j9V1L-N@7{nRYSkL6S~2IU zxz?CttT{%Xy^D#3#eEv{Tx~?>O3U@$j#0u6YyN&}-PHbAL8}L~5w|w+S^Y^V)R9sq zHWQOE>RCE5!Uml#+Rl(tT*j`>1O}g6@;$4rN}^CE7QQ9pfPy-AJShj@0i!Bt$}z#( z{${s<%Hz1k+c4Rk4R5V7J!x0+16!gE3Hh=cXBI(2OO-X!%@J%&pFFU5uttDP5*tux zFs5NR@sZ-fcnR*kfg7bKzYc4Ix|!L5Kgqm|k1Cf!T25>0H)kq|W7i(UFC`VZTh4`; zssv&_g1r*9ds!+5bIKMC$}9d1tubw8k~h<=D5Zau6ql_-T4rf+hhznWyZxd{`x7EW z@%Z@)>7@6e`NqrlGw=@^H253ayOo|Me>4a=H*1gQOh1D`HxAJDjl?Pz{&;at~{EXV-p&X_J^)v<9cZXu_k&lAc% z+znf|jY~a!5?DrK9UAh3%kNdrpft~x)BcclrdhjBXtlM>wPr)-7O;WTGSf$1Dzld` zr>`)H9UsCq0xD0|`1h9!=tUS=tF?Sp?EsD~o-JNtV35epmLfV@iY>~7LVJL%G}=~o zM#5GF{pE$p_Oy<-le3WYF*1>xI(Y1~^yvtuD{f4!F8*algatw0YK|dZoCSa#XYdFm z6TtsjC8^9kQ76iscrlwR(XMflDt>0HvRp2@aaJ4t?8Cxi5f0{jhthcH+VG%-cI#>> z@_5pM`%4|Vh2`T~U4PVKKz%WMWX)TI9Peg><9k;xGIG_J5<1dHz{Ds*VFN%zOwzs; zXN4h_a|eyI{bMjGvuZN(+=7BbNAs!fC3UvwBzNhkQ$rKdD5(^o3!WhNr8GMs7TkN7ukAx^$17g(aSia`6~DubhP+QPqE^$)kr zEBk`_%IM4;Ihcu5GLn{rp5?a}hS1k3al&Ml3dlh%=0n@IOKK1`4j)?ToL4ElRi?vP|GQ!DEl=_XhNMxEq$SF z;90Ur#G1A@)qmMStKfKQ%!eU?wsTbzZ8gW4njuM$W&r@O&<^wHp$@?kofc4I=v&_E ze2rQc3Sd<0Bh2+p_Plht%*s)lZ_%PDC#}y1W@pk9GOq_Ns9Xu#&ADeqx5dLLw!9+dz}5A9Iq607bU_NpfGh)OUg`d4?a1Tcg#45R|Ss)YN%h7 zIGyz|$!?#d#vL7w-AHZC3d)RAm4LX817moxHf|H>?$RgnI?V3sPUX%4Um4x^tcKV| zW@--LAffDCM6263b2bje+yibhHB9e1=$EAcW`zo)D#7Np$?Fr@WV&WVUQAYSQwZUO z9tsPXtblQdT?7QMd;v20*OTss14nS245S!#3u}|1dNfnD70XLK#LzTsgmZPT7vV&Z zOu&8ebj$n1aCa1gkXg>AdHGR!)T4=N$esCJFRB?`hOq!cMW$g|7FB8niZq7H2AEV1 z;t1!uRUH=`Ds3kdiKDkfEV-C2mncxhUn7}GMb}S|TnZSm4_h;npoWfqV;{ubLErWq zu@rZsAYeo_?foNRTwlo)Y?kjru8c=#LGC!DD$#=C#;>UxXRN3^wS3u$(B1ahLx&sH z+AY+4W?r3O&Akuf$igBJse~Y$YW@e)M^?l{0j}13OI_sJ*p@(XAGQEZ=t)**hZD22 zf*8?x%hhsgpqMXu!$qG;^$0sk`IruTPZA|S$Qw7B+RNKBtXPr@znve;Aleu|#w51H zwRbd)a~y-fCIO~f%E%Y;JBiZe_+(9HCTT&7mf>-sxwewdMSMyCFmO{LQ&yjlm_r!BMHzJ=?HEvCTA9JPCsKr%sw zqPneKwolcI7D~38hJ?cOvh52O*NH!Or(L!Evf=!logu1e#;*zY-{jtZIaG1|Z-Z^W zUOP%KV598W(YJyN;A`@=B`jiq+fh(aW5#z+%-7{k&2UGEv?9mFQz8DA@-{wRaf2HY zt=4gP2O%nY_3uF5C5PKSZ1+)?-wmcuo;`NrfAb0AqCM?tfa8Jr@a9K==h~RRDgHCA z>*IS7K?jSsY*9ZVTm3q7-|*Cvqz$~6Toqpbib}W`J7)Se|MZi?!K>)w!9BK5lO9S< z-=PO?Ot0|19JG}zKS2Ihp8$FpPf*J&onG9p-1Ae?P$G;OV{}KDRenRsA8Z!B9*+u!! z_;`;OKaz#&Tph(Wd_X+Ll({cy9AHjiZ{_E^3g3#?m(UZ3z|6;UoZHFkIDzycKWCH_`%PQP9o?)7err{?~dnD%G;DLj2$lSt!iGcWykoAeEhN=Op`!i zr%tHUq;5!BQC^&D$JJ6Tn;{UbUWU!3p&40U$z3}xHB%hyT0LGlIR%i3rR#9{*s$s> z%AT_%?*Wog;WS3VON}loBV~uo*|UHl9dR4gDJ&~wY|D%fN(uw)?TRZogK-Qh`=F7L z%s9=((`D{lZUI&n)ny5*h=&I&*E0jR*39-t@f>c*&ewo99#-UgUsmKkKq_yQ+hf#C zn(Ae9tz(GG%&pkc8P7^pAJyRs091>0-K~0=^z<;)+asx~`ODY!xF6(~X^#wBH+PQVXX%0J-T0gX??;h)B+3ENZ<`=bgbfgg$3 z=ZX(SWjWo1OTw+$sT9$#DBiwqiL$l$8Zm22Kph8lC4#J};yZk(2X;}BeM_3!PmoWHH99u z0U0q##%~O1SSZS3*7{!LPynCs!Z6bT*|%u@kB_tIudKNs6MDRyeWg&O;C6ttnQRRN zh1pz+AsIe=t;195iFEdfPRy`QwiPh}?i9|JOhb-{it@ARyG~B{5jD%{HGPO7?9wdA z^6}Sx^~Da>7`kzk1{o5J=Rxk%N#fSoNWDlK4QKB)Bgz?MFuX0cM-?b>`IiN`66|W0 zY}uwOPY}2isS3a=m9zWiT?L6%V?dkoSH_K}ir#W@!AIO@kI4xLm8_~u&wb9eW0L6B znoaTpGh|m^*E)GDO_5-AL>1X=JL|+g=!98)q}dOU#U;YruC5Nmm>{%&UXVH$z)Dkc zCAT!`q)AC;%9X2#PWS2|$Q!-@s|p^qpzEE=tRBEr_0<9dw-0j6%*k+a6a=|Gywe%K zTf%lO9*5Pvc8}c9al!5F;vT$$7nEO041(-*$nljM|6r-b&>iABVnirRgGv;z6jh zuXvl_d9X)IXA9~m+}=rQkW3KnFtx;^lb1INg4@C)8FUv5Zh#??&l zg*aw|u#;LmdBSbF@(lY$&0gRG@SgP@pw3^>uZl-s?i2xp&qCOzP`0^KvF<7)#~^IE z&_5y3Vehp1Dx2Y0CL}DC3|+*zL0nW3`V{$Bs)D~BeA1Vp3mtsS-?ykUJw|c7>}~ET z@rB$@zH|5Tbo>cX3HqauW4Yc6)=!9Wk|XhXh6vyD)wYwL5ZJ}|KBjjgKmzHK$Zc#0+eH)>CDu^M$%WblUp5?@d01W7a>PjLExH%i9*$>9hUkLwD(iV(O zWNbuM@rie}RveJVdJ+qW0q|l^&uPG{d7R2Dh=+gJwcU){fb5flftzT-A#0IkhZX16 zKu6}Lha6}Cv#a^xE2s(1Ma47oO_dOZm#$J;3ikFH&#ZbvAUHZdel`Z`*K}DAJBE7@ z#*2bxeW;m1NsJK{M&w1qc#8P&=R2~D_?)1G)+&*Va$xs*o2od`hNTF~VsRNhJ1Z4C z^fdvFC^9K_q|9)CBtvmI2pp=*yebN-oNq54pq8Dg>J$1|UgLvgNLLQs5JO-9L=lHF zR$eVK8YLX;CI-^9C3#z|va%j+jegidX<-pHz6}Ij1#gTR*9OrZ);YVfLyPbU5FMg> z42~K8c8uWyis-jbO2U?>;C@+0&PqH4l2RCc?>4I&*%K~ucNG~!EUpSsXriv(WHZsxS9m0A&QNFKUz#|h2IO1?C>jsO^-te+G^GyE)7~4e7^l!gY`qaJ z4UqTQ^?`$0G7-zcH~!=*M}7;j7{^@vg7tcS(MqeRt`wp#AF;fqxl4TxM5%swd-lGe zVOArq|5w}m1)uO|TwYV>>Gawge7M~D&?1&UAu6EB;CT7YPnuluns-C>->%!vPAPHS zx=^K)auTUrUF0wLux1~~GI^Mitpp|QSn(4w!1OC>**9m(QK1wtkhl|vgUT8}7GicPzLeE*$~Qf) zMxp_yLb=F_Q8D32d?r^3P74=p9>nX*GgV3mYI$1TRavMo2dtj3aBETpYF#_im?X(oR zy|ArBXi2v&Ww}!>y-*Ug3faq#S)e_A6yCgfB91J!@<7j{wM*;%MlgLV+dwO*rG@-9 zPQL7o9MQ&^&*}p0B8$JY5dJz*{1s+Zf*RS+_ z3Bw+&j_}xt_NP>1nsw8%>RwMa%v{k+AVtY{7zg6_dHa>||AYvI>{@E7iiF69xxY?Y zWv4jo^5`rV&>Am=MDa}R>ifjhWU5n`GGIp~0_SEvjo6?Yzlq1KGC$R%jvb37gFd{c zo8TF>P&k@=%xE9=mGvuijH-wq&rFZ5EnjS&1GM8f5$)hAZ>}& zlpBu`t0PC5x1wcoY%gC|&NWpYR9Z?0x-#2nl@z(?4Sl>wq(Dd`5Hzh+DU_ppyR(;P z8pzO2z-DF^^fBII%T}25BN_PokWt%$#15Q!K)K{J;=R?@e`n0QI+udXzc$ier{A9u z++|e{QN#H+01%|)*S`kY@1gxcB>e+j{Kuu84j=dB^=Lc!y@>{k*(uAEzrvrR^IO!Vm@}BN@ggda1{4s>$o4-JlsmM|gFvbKMZ89H7Ii$b0_c#s zg>|GB4SO(z0uw}UJc2eoc+a6HlWLxaHci1gjMyV&X?AcDbrBlH9#NB>K?HKD>0N#_C^gG_PuJFDc9|GZ|V$)p0Xz-0#rZ^D=K*v0T zK+NB7C%y|(5MR!)pV^lY6&}vwqGsk*i_}X%OGN{<%($uE$K;xkCM_{u-NoT+p#h6B zqy-PNHomp{^<4UQ&bLK zUIMzZt3<;U{UT0H&mQ(nC|8#;JKK}c<1QrVw+l;xvN-`sz*NeOiUZXrw|q2gHhjlJ zpaL;~ns=U67@>)z4JoV_!RL0`TpWJQcx}T>sJqiAmm37tZClcMw*CUq9eJ3Z`n#pJ zoPvf9(r?f6&q&H_5RKqQJQkPL5s|4s_!-Z%$;GxW{2|G*!rm5^#>_&9GxM&xsHT=FV{9ht^-uL~Ww-Uz%k!ztx=qA?xiEewK! zY;Z=jH03)8-=VsK?8ewdS*2eFhqZv z3==vDZ2`sES}Iu+(7Mf=lY(Ag9{0keH-HH9eL58KMZUtS$Q*C%!u_z0odRo9+Na-N zz&eBeRD{)Fl9Y?XE%?x+NeWh+buNdg7SADWei@+!UsrlO?WkKK-Zv9;Lk$;qySNIB z3YV2tmFnr9ymTd;iAA1BX3vTv}pJw5_csA)^L6*9`ivmZi=jECQNlj&Bu)?G(DfY?U;;ZZwxT|ltj6-9X9!-MJ zDp_W`0mE5ngo(bW5dWLwsL-a0OdzCBq9tN5DaMl%OUa( z{GTnOjU{fQs*aH(gX(6Imu5nnOIaVH)=bLtKbimyR8Btew6T@y{{Qx_k|n<$g93({ z(EYZx)F|j$LAnVrLHtI14Di7EwpNKMtL$NS7kWK~{D%p*ffmR{s8!MN*K>O@?kdf* z(7t)9#l1qW`_`_eax&%eQ9AAl!#@i3SZ&yn-oM35SUCB>I1e`tIznjW^GRH2=;v{fPTcf)EW6yWvDE4Z^E)iVLt0! zT(DfYj}l9^NBN9LvQUyRq5kZA8F)5|{+{js_Car7VdX31>5E3hAtKog+=oH>vNcxz zc-*#GTypxMTRj`ESCO_&cWZN>6&;bV8h)W*EN!u8L=6sJ`_fkQquFE_6*OPRyzD?qqM{QI63t>__L= z(_hk+?5?cc`47c>9)ycn8cf!j?-6RE?%9qQ z&CTa#YhSiDxWsTmKpPQTB{m%r@%R=s3{#I4>ui7ZjVtGLy*^mWi~TYmebiDkZ*q&> za;4e=95(+6LF=jx2z97zm}629qUvw7Po5dINl>>_kfi6}AJmc`WDLhtsQ}q9XpAKj z?5hv2k){-6L#CidjZwD`t~KT5$9w4ygv zRox$_tR~voB8} z^|9%wC>%3q%C4XF3jFSVsxny3k{i*LD1J#FG&~QU?$;3o@{SirQ!r*}zhmEQQPptk z>}n_a7dP?0xQYLbn;2&}oK$+UEJj6?gbASmfI4Va9zJ#O(e3$5O`?EARB9C)lzsD; zP*00u&kLGx z_lB$2{Hx=ScNE4wPo6iXz~-eD_RWF$I%m7xqf~Fn%RZ3d@QP6(*Po(JNaDJJ%+Tf1 z;3;6oU`QzfSUn@)yCS2{vB<I;x=^X;iK>R(1xmXueKSI4;NF*){qg}we00_Ef7&OsMThjpJ?HD{$Thr)DTg`En z)WRc!)9?poZ0ny84>=FRJZHNsbKbuA13w{fk7M)0g!2r&1$;I$t6Q<(1Gy3$)<=vg zJUOC&uq={DqWOts6Stp_&#%7j1QYywQ^DV~RE$}>vp6~pFvhx+-c=wa{! z@A}qr{CAaCGjx;7>qx79U&|kjD-!*Gs6Lx^XE|K>4?WO+_=a_Gau#aeaiH;BGY9B= zPv>8|F~M`!Y37F|di~nf{kE6K$d&%bf-U8GXHuv3xh3I~QJruE=|c$r|Ft8mKPRAz zj9NiVi%z3hDa$X3)VFZ))Jx?T>5_)ef^JhBZ6}}xqA6grk~q`3K*I3yZIF8II!LeC zUp9^<9686w*ffs&xydcS*{+&ec_YR-ZZ4|WKox1 z3#|bu)dz~QcS@KA@Z2JHNpKBw77ciPs6}-d(O_?r^7y$+HAP#pVxA$|wsm_+>B!AF z-+X{-$OPXT#p}irwMxe>6T0TCh;NY*dogsMIIWbqb^qf_9N%NtSf4?n>Tz`ej)^vE?6ZIC{hje4kX#4OKKZ$Co%m9bBhot4A=X@JV28pG8-?mbldBM)6yAdbY2( z{c!I>{}IOW>i6kL{Bg{X1d=0mz3h&0Nn;iQ719_ex$Hi0p$ep+_{g_1GxTw@5@&{d z`og^^vvb+9E&LDzaW#8TN=%iI{v@*{jR7{UTa`_TL6yRc(Xsw$HcIAelX7aVny)5; zLfT8q2Hg<{MG~4qv8Kt1qMY_)2YW#h+B+t@qIp7_tAc8fEuhTg4Ae??;HL{3Jvn#)EWRrvp z>-KIIy#9<5XMQn3x|f%T=WDhP%N%Q@SMv9)`Ce@S@7Hqt{V3t*oSmUf1JiYpVh5+nH(AC63j z0Ze{RXJ8_4$EPL3whG)E?!P+t;#+5D9yfT30u5=}xTbS{U;S18@GG(U`83La>KBbx ztrG9XP5h*=^8=L3gBky{gO~Sd@)eVWMid{IYm87h zXkGU+`_%e(H$R4$?5b+eg4PC8l()-5)ZUpSm1an~=9_IZdzFqPjJE>6I5#7fa_S!r zB*Z0SCCL&wut2a0KgHytK^W%|f19_~LDj(^a^7Jf0KWOsHJNoeHV5019b;m~{(+qJ z^kIm*m-PbtxOf^Dq5UDLedSQTr|?U_tiDQ%7+DDkL>!?+Ex2}uyM&jb4UMFrO!Vt( zseU5b(1@gb!I&v6&G$_gc9QS*I2b{BOipXKt1Z*8>WoJSF?J93t(_J;@b3UCC36OlA&4(FU zlS#_>8f1fbi_l{qwlPN+mB|!-vPjo2D^@Sy>VM4rZ?kX(9ovhW06jzlVarPfGMud`W^@zh;^?=R)iaZHPE%G zFP)M8O5HVE{a@2j~cEGv$U2>dLqvXxaq3$-((ipZoR?iR_KZB zUzCu%*5DetXG;gc5FTT@OheVWj5W?yWJQ2fk2RM~`pd%=sCZN}<6YItPwE(@0YUF< z6%|!z^XMrvcEMxDH7sBA6mr9_A!fW)`N?nrUlu`@UPn*f!-{5LFw^8~EXqaR^F=sv zw;T-FKF@VkIco5wJ+Xla(u_;1$SR(X^0`4;fecuAZkWXIBHd)aZ9?cg=`>>H3}Wp< zuj5b^3-}=WVNG?#Vwu%CthG0B8+HaE{UmV}ekUSsOFU-2z%jh7tL4S>{#7#gcN!ev zS2AliBR?UWOZ9H?EMCApA&PgILRYMD7_dTALvVmEmDQYwgBpcL{q&8Lic$oRlPR4nB7nYAWQipitV88TUh7ud zp26M(sPrzjkncMciflXJ<4+|&wb*2jP58``^uYgGUTh=<_K*kr^1-1Kv9&U#jq29 zls)16f=K=e!QqzZcxDG)LKK3fs+tGD!nOv%AC?{*7Rcn51W`_!In}m>g=M>~x@3V~ zDEu@us}S;*)7FzdW%|y(v*yTrXK9|D4<|QoIaW!Z9JI};b4f6Dr$EYcz40_b+#%LT^eY0hcT5sMYnyLqb&2IWrncY}5 zX|aE(>@4k~CpAtm&%5mo0XaRV6)3Vzfo4PNCu70e;KF$9RA;iR1<7WA;(3cQ;>Za* z`-E^d0c0TJ_(0EmJ(3%F)HX(`;(YCG7>O*!eNkfG`3p!2YixbN!!;1kmXdbLKyRLj zes%Rzv;v8FefZ#rS3*3gowGkUJcfN)RC&>2Ic^Dhr4=EGt3R|(1@^lW3Zx$Fya6a7 z_mG*@Sn8p}nZWN@Md2ve3?a|mKYACppkZLYo~OSB30PeExzU)*h8oE&-zF}9CC*0C ztrIWITEV02S}&`<{(U?9)Io zoE@Qj6CtB`9h{>=d^6urx~DU<>N*$3x&-pil|C+cQ)?$J!P^*KNmILqQMtKWBJ#Y%uHlBzG#_oAdWpVeUypUt%6KrJq*R!a$#)0 zOfpGjPyt9&4p~TXDKs=C^_9FXhf#%cMmuHKLsHp(_ zufA~mvgQ*^#L28MiutRKqSQJin?3iE_e)2#NM9MJ5lV!`)F$#iQ_-gIFh+D%Pn__^ zBYqRNk#Oo~Gpz-k+wfms?5$epWN0|k2Cbh^;v=xlC3FxR8R@SSNU0pA8$7I{t)%!O z3lIaBllRuXbMN|T>|ACpE|b(n4Of<-$aAcull7m?)iH>n$0%_X<=ZRZbEga%iPFK5 z$|3FU%&D(r5$;^LmkwD6@*n_Sb@d-f)em3DCzixTOj9wzqUwh(D^z-wN)@h=+9oNx zSdd^$t0K+FKrZasHU@e(Y=sI&z0hKQ8zCp;;)RxyH}ma4!*41W@1g()3P5{61yl-f$->SI9}a;qaymJUZS!?rWOiaxH{ zGMoTjP+?_=7seH;Smf_1L}c0q2;A<*RUcT0AMv#Sf}XvKo%kRl$?Z;4+E-DWM;94I z-wg$B*aQCm_yvL#qKb+k6D2<`N?7Tpi%eiCGbjpyJ)f3{f>=6w%G6ASvleTreG?m* zHsk}jOsSZO>VQ<2T0sW85L1!^_n}|Jhkduc*HsHy5_kHFja}#=FKb6QST!UVeyJYg z823F8?q&}6$coQ54+q4h=k}%-oSokvF&b{RK0%-E3!bgKcb$}S2%G#mw;Fm`fba1Y zf*Ro1{>B9J*H)>opKFc2jjH|dfPO4Jt@;T;BWyS8gZ*~m0KnN^>lu08FqJPKxwD$= z%$~Haj82?jZpUcuz63QKL~oo$sRa#zr=h?rjHit*(Zqe0HQhGasgLq(i(p%rV>h_z z9zC4|p}@b%+#s1p(sKV05o8?vA{m%NFEPwG9UwsqWCh%Izk*+vGf#ENA%oWwzK9!u zSal#FgNa>EhZeX}ox;q#tdPxEes zF@*(ubkJ0lscC$lEoK+yhV3DGu=j?NEBv&}-l?{dvc@nrlH_GYGKUpa_zp0At1p!x zg0ETO9h!xdDm=O}6f=V0nQzZf7Z$ARZ()1C%u^C_ZuG}msQ2xNsox3J@@RHr+ts|TS%_XfJU%|Gk7{e+mZ9B(x;$-Q57f+`2opbO*T!2QbaY3dsj-&jBNwnd#~5YgD@7kUH4rHI!s z;K(|KE&><|`VJP{EA>?~jA$Ud{CS7*rVr!- zK+;pSWGIrnJPe>kfnS!0zT(0T;>#RZ$QZl#O=3nLi`up|F{QvmaH?&rpt@`nf^#fw(Y{&U5EHZ>X6K^2UmnAwKm}9I$mPm| zBQif&y}&~tD_hcAbhnV7z+Ie~QL5Q^oWOI#&Zb?SDID{S!64H`*pi%11ff|q#*EJ= zZ73+5A%GyCz6%arD_#MQR{sVD7j0II78F$!E^Z!*%_pCymb<~~frTVcuGKyN6tb(> zuSdne>h`jsOR0ohb=9#E){q>mB{tYnSr+7@&xIPEV6`t(PF_?NTay95BWYu`lLs={ zo9U1^6{fC;_Sl!SVL2J|c@X*d3G;j0d;11|iAA;RhLKmFS0taB2W#ou6;w{xU8H5u zuD1LWP*0|ZGU@%n=_0q8VV$YS;Tqp;VyzQ1`2W^C=eMnavejzpKJrb04>E6?l*oZ)4@>6IYxd+n^tq|D%0NQJidKhEZ+w-eM4wx| zFwK?brfJ_?IpJPz{%g?x^E>T{!mAp-2k9$smYeUs9hwpm0~u1ZTOc`t_ck4dlQTCO zR8hK;Cr%NZlc790Spy*#4Cgm~Epz{7|^leZEk4cO6n5p(> z-Xte}eHsJHI7Z)uTx8|3k|K%{QkYzcGUq+{;FK_csB}0T%oe}7&@S%D%3zD>H5*l+>+)MXY z0m09_@8@R!OA8)<@5}z4g7SY8eEc%R|CjCXfA{de^U3=UrMQ1MF0O0GOSDbGcf7eq znY2c2ue;V6mOD!HS~}mu0C|mK-;CL%66%HO^Df=|LFOm)KQmeR7p^iiRM~%DneDGw z-G2p3{Xe{Af91#gqYDa$E`7fB_B?{*Hqqvfj~7Jif{+qH=40<1$%1O;z95#+8hym1 zFt=ZkGa_8NTbpk@@macQx4l4l(>`l>s)2R0ly00-^d&}$3%lB;z(WlrWELYC|1C;G zQsWdqCXK>hklE&W{LbjiIoAA6_lf0}#r)22e)F!{_KB-#2~4(;(IMwGAf%o-==n+> zeY-cjil_0+*3~(daA}2i{Ug_A#%{Nz+!*J2%uMpr%+%}?#MByg;I%1S z6X58~6EZyYzAZz41t9^q*`c~5oXZ@X;!gWpaM?Oef#>hmTj(en)AFo&uo82F~`sW=^e+vArt>HYEkKu})4-M^$u|1_W-dEdI|Ka9C7sIc|B4%{D_~SmV z6fFAVbRs^FF8bp(-uW{u{e(c~t_<^}u1;8UoCSRL2ypFwBgmVv?PGY;Bu#tKUfCOc z=VCuydO|IpPx@o;do zDUvIF^k+|eei17!@G+dfG@RdS2Y3F#mQnscXFBKKCVH<1^~Dqr@mTmNlCs`#b z7`SOd;n^JbO*yT60S~<*^#8;3YwafVvbR|UH`zExw>ftr>YW=go%!g_-8uMU{jOW+ zoK&i3mc3mPjo(i*VkaH49NzsAZ{4Y&>z%YCQP`C-!m`jfQ=UGHzZB{}0B@|1SFOyruk>XY+mgh*0XL?Rn=) zd+Mf-(Z{X!>htN}dCC8?zSphaEYx#52OobFsS;Q_igk@tJ4#eJuxRRoz3F4P3T|&> zv0wh98~>*r&A%eaKV4w_S0wohf$?&MF;zVxRf*h9oE1QVCsHFPlzraYS-iO}kTt4T zA7k)^&`Y3-1lc1EW6~KsJKxb7V`BkOAjCUVK_}Wt< + + + + 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__ +]);