├── composer.assets.json ├── .phpunit-watcher.yml ├── config └── tests.php ├── src ├── JqueryAsset.php ├── YiiAsset.php ├── ActiveFormAsset.php ├── GridViewAsset.php ├── ValidationAsset.php ├── PunycodeAsset.php ├── Validators │ └── Client │ │ ├── FilterValidator.php │ │ ├── RegularExpressionValidator.php │ │ ├── CaptchaClientValidator.php │ │ ├── BooleanValidator.php │ │ ├── RequiredValidator.php │ │ ├── EmailValidator.php │ │ ├── RangeValidator.php │ │ ├── UrlValidator.php │ │ ├── CompareValidator.php │ │ ├── StringValidator.php │ │ ├── IpValidator.php │ │ ├── NumberValidator.php │ │ ├── ImageValidator.php │ │ └── FileValidator.php ├── GridViewClientScript.php ├── ActiveFormClientScript.php └── assets │ ├── yii.gridView.js │ ├── yii.validation.js │ ├── yii.js │ └── yii.activeForm.js ├── package.json ├── README.md ├── LICENSE.md ├── .styleci.yml ├── composer.json └── .travis.yml /composer.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "npm-asset/jquery": "3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", 4 | "npm-asset/punycode": "1.3.*" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /config/tests.php: -------------------------------------------------------------------------------- 1 | [ 9 | '@root' => dirname(__DIR__, 1), 10 | '@public' => '@root/tests/public', 11 | '@basePath' => '@public/assets', 12 | '@baseUrl' => '/baseUrl', 13 | '@npm' => '@root/node_modules', 14 | ], 15 | ]; 16 | -------------------------------------------------------------------------------- /src/JqueryAsset.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | This package is deprecated. 4 | 5 |

6 |

7 | ❌ 8 |

9 | 10 | --- 11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 |

Yii Framework jQuery Extension

20 |
21 |

22 | 23 | This extension provides the [jQuery] for the [Yii framework]. 24 | 25 | [jQuery]: https://jquery.com/ 26 | [Yii Framework]: https://www.yiiframework.com/ 27 | 28 | For license information check the [LICENSE](LICENSE.md)-file. 29 | 30 | Documentation is at [docs/guide/README.md](docs/guide/README.md). 31 | 32 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/yii-jquery/v/stable.png)](https://packagist.org/packages/yiisoft/yii-jquery) 33 | [![Total Downloads](https://poser.pugx.org/yiisoft/yii-jquery/downloads.png)](https://packagist.org/packages/yiisoft/yii-jquery) 34 | [![Build Status](https://travis-ci.com/yiisoft/yii-jquery.svg?branch=master)](https://travis-ci.com/yiisoft/yii-jquery) 35 | 36 | 37 | Installation 38 | ------------ 39 | 40 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 41 | 42 | ``` 43 | php composer.phar require --prefer-dist yiisoft/yii-jquery 44 | ``` 45 | -------------------------------------------------------------------------------- /src/Validators/Client/FilterValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 25 | 26 | return 'value = yii.validation.trim($form, attribute, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 27 | } 28 | 29 | /** 30 | * Returns the client-side validation options. 31 | * 32 | * @param \yii\validators\FilterValidator $validator the server-side validator. 33 | * @param \yii\base\Model $model the model being validated 34 | * @param string $attribute the attribute name being validated 35 | * 36 | * @return array the client-side validation options 37 | */ 38 | public function getClientOptions($validator, $model, $attribute) 39 | { 40 | $options = []; 41 | 42 | if ($validator->skipOnEmpty) { 43 | $options['skipOnEmpty'] = 1; 44 | } 45 | 46 | return $options; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/Validators/Client/RegularExpressionValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 28 | 29 | return 'yii.validation.regularExpression(value, messages, ' . Json::htmlEncode($options) . ');'; 30 | } 31 | 32 | /** 33 | * Returns the client-side validation options. 34 | * 35 | * @param \yii\validators\RegularExpressionValidator $validator the server-side validator. 36 | * @param \yii\base\Model $model the model being validated 37 | * @param string $attribute the attribute name being validated 38 | * 39 | * @return array the client-side validation options 40 | */ 41 | public function getClientOptions($validator, $model, $attribute) 42 | { 43 | $pattern = Html::escapeJsRegularExpression($validator->pattern); 44 | 45 | $options = [ 46 | 'pattern' => new JsExpression($pattern), 47 | 'not' => $validator->not, 48 | 'message' => $validator->formatMessage($validator->message, [ 49 | 'attribute' => $model->getAttributeLabel($attribute), 50 | ]), 51 | ]; 52 | 53 | if ($validator->skipOnEmpty) { 54 | $options['skipOnEmpty'] = 1; 55 | } 56 | 57 | return $options; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Validators/Client/CaptchaClientValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 25 | return 'yii.validation.captcha(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 26 | } 27 | 28 | /** 29 | * Returns the client-side validation options. 30 | * 31 | * @param \Yiisoft\Yii\Captcha\CaptchaValidator $validator the server-side validator. 32 | * @param \yii\base\Model $model the model being validated 33 | * @param string $attribute the attribute name being validated 34 | * 35 | * @return array the client-side validation options 36 | */ 37 | public function getClientOptions($validator, $model, $attribute) 38 | { 39 | $captcha = $validator->createCaptchaAction(); 40 | $code = $captcha->getVerifyCode(false); 41 | $hash = $captcha->generateValidationHash($validator->caseSensitive ? $code : strtolower($code)); 42 | $options = [ 43 | 'hash' => $hash, 44 | 'hashKey' => 'yiiCaptcha/' . $captcha->getUniqueId(), 45 | 'caseSensitive' => $validator->caseSensitive, 46 | 'message' => $validator->formatMessage($validator->message, [ 47 | 'attribute' => $model->getAttributeLabel($attribute), 48 | ]), 49 | ]; 50 | if ($validator->skipOnEmpty) { 51 | $options['skipOnEmpty'] = 1; 52 | } 53 | 54 | return $options; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Validators/Client/BooleanValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 25 | return 'yii.validation.boolean(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 26 | } 27 | 28 | /** 29 | * Returns the client-side validation options. 30 | * 31 | * @param \yii\validators\BooleanValidator $validator the server-side validator. 32 | * @param \yii\base\Model $model the model being validated 33 | * @param string $attribute the attribute name being validated 34 | * 35 | * @return array the client-side validation options 36 | */ 37 | public function getClientOptions($validator, $model, $attribute) 38 | { 39 | $options = [ 40 | 'trueValue' => $validator->trueValue, 41 | 'falseValue' => $validator->falseValue, 42 | 'message' => $validator->formatMessage($validator->message, [ 43 | 'attribute' => $model->getAttributeLabel($attribute), 44 | 'true' => $validator->trueValue === true ? 'true' : $validator->trueValue, 45 | 'false' => $validator->falseValue === false ? 'false' : $validator->falseValue, 46 | ]), 47 | ]; 48 | if ($validator->skipOnEmpty) { 49 | $options['skipOnEmpty'] = 1; 50 | } 51 | if ($validator->strict) { 52 | $options['strict'] = 1; 53 | } 54 | 55 | return $options; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Validators/Client/RequiredValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 25 | 26 | return 'yii.validation.required(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 27 | } 28 | 29 | /** 30 | * Returns the client-side validation options. 31 | * 32 | * @param \yii\validators\RequiredValidator $validator the server-side validator. 33 | * @param \yii\base\Model $model the model being validated 34 | * @param string $attribute the attribute name being validated 35 | * 36 | * @return array the client-side validation options 37 | */ 38 | public function getClientOptions($validator, $model, $attribute) 39 | { 40 | $options = []; 41 | 42 | if ($validator->requiredValue !== null) { 43 | $options['message'] = $validator->formatMessage($validator->message, [ 44 | 'requiredValue' => $validator->requiredValue, 45 | ]); 46 | $options['requiredValue'] = $validator->requiredValue; 47 | } else { 48 | $options['message'] = $validator->message; 49 | } 50 | 51 | if ($validator->strict) { 52 | $options['strict'] = 1; 53 | } 54 | 55 | $options['message'] = $validator->formatMessage($options['message'], [ 56 | 'attribute' => $model->getAttributeLabel($attribute), 57 | ]); 58 | 59 | return $options; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Validators/Client/EmailValidator.php: -------------------------------------------------------------------------------- 1 | enableIDN) { 30 | PunycodeAsset::register($view); 31 | } 32 | 33 | $options = $this->getClientOptions($validator, $model, $attribute); 34 | 35 | return 'yii.validation.email(value, messages, ' . Json::htmlEncode($options) . ');'; 36 | } 37 | 38 | /** 39 | * Returns the client-side validation options. 40 | * 41 | * @param \yii\validators\EmailValidator $validator the server-side validator. 42 | * @param \yii\base\Model $model the model being validated 43 | * @param string $attribute the attribute name being validated 44 | * 45 | * @return array the client-side validation options 46 | */ 47 | public function getClientOptions($validator, $model, $attribute) 48 | { 49 | $options = [ 50 | 'pattern' => new JsExpression($validator->pattern), 51 | 'fullPattern' => new JsExpression($validator->fullPattern), 52 | 'allowName' => $validator->allowName, 53 | 'message' => $validator->formatMessage($validator->message, [ 54 | 'attribute' => $model->getAttributeLabel($attribute), 55 | ]), 56 | 'enableIDN' => (bool)$validator->enableIDN, 57 | ]; 58 | 59 | if ($validator->skipOnEmpty) { 60 | $options['skipOnEmpty'] = 1; 61 | } 62 | 63 | return $options; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Validators/Client/RangeValidator.php: -------------------------------------------------------------------------------- 1 | range instanceof \Closure) { 25 | $validator->range = ($validator->range)($model, $attribute); 26 | } 27 | 28 | ValidationAsset::register($view); 29 | 30 | $options = $this->getClientOptions($validator, $model, $attribute); 31 | 32 | return 'yii.validation.range(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 33 | } 34 | 35 | /** 36 | * Returns the client-side validation options. 37 | * 38 | * @param \yii\validators\RangeValidator $validator the server-side validator. 39 | * @param \yii\base\Model $model the model being validated 40 | * @param string $attribute the attribute name being validated 41 | * 42 | * @return array the client-side validation options 43 | */ 44 | public function getClientOptions($validator, $model, $attribute) 45 | { 46 | $range = []; 47 | 48 | foreach ($validator->range as $value) { 49 | $range[] = (string) $value; 50 | } 51 | 52 | $options = [ 53 | 'range' => $range, 54 | 'not' => $validator->not, 55 | 'message' => $validator->formatMessage($validator->message, [ 56 | 'attribute' => $model->getAttributeLabel($attribute), 57 | ]), 58 | ]; 59 | 60 | if ($validator->skipOnEmpty) { 61 | $options['skipOnEmpty'] = 1; 62 | } 63 | 64 | if ($validator->allowArray) { 65 | $options['allowArray'] = 1; 66 | } 67 | 68 | return $options; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | - resources 11 | - views 12 | - public 13 | - templates 14 | not-name: 15 | - UnionCar.php 16 | - TimerUnionTypes.php 17 | - schema1.php 18 | 19 | enabled: 20 | - alpha_ordered_traits 21 | - array_indentation 22 | - array_push 23 | - combine_consecutive_issets 24 | - combine_consecutive_unsets 25 | - combine_nested_dirname 26 | - declare_strict_types 27 | - dir_constant 28 | - fully_qualified_strict_types 29 | - function_to_constant 30 | - hash_to_slash_comment 31 | - is_null 32 | - logical_operators 33 | - magic_constant_casing 34 | - magic_method_casing 35 | - method_separation 36 | - modernize_types_casting 37 | - native_function_casing 38 | - native_function_type_declaration_casing 39 | - no_alias_functions 40 | - no_empty_comment 41 | - no_empty_phpdoc 42 | - no_empty_statement 43 | - no_extra_block_blank_lines 44 | - no_short_bool_cast 45 | - no_superfluous_elseif 46 | - no_unneeded_control_parentheses 47 | - no_unneeded_curly_braces 48 | - no_unneeded_final_method 49 | - no_unset_cast 50 | - no_unused_imports 51 | - no_unused_lambda_imports 52 | - no_useless_else 53 | - no_useless_return 54 | - normalize_index_brace 55 | - php_unit_dedicate_assert 56 | - php_unit_dedicate_assert_internal_type 57 | - php_unit_expectation 58 | - php_unit_mock 59 | - php_unit_mock_short_will_return 60 | - php_unit_namespaced 61 | - php_unit_no_expectation_annotation 62 | - phpdoc_no_empty_return 63 | - phpdoc_no_useless_inheritdoc 64 | - phpdoc_order 65 | - phpdoc_property 66 | - phpdoc_scalar 67 | - phpdoc_separation 68 | - phpdoc_singular_inheritdoc 69 | - phpdoc_trim 70 | - phpdoc_trim_consecutive_blank_line_separation 71 | - phpdoc_type_to_var 72 | - phpdoc_types 73 | - phpdoc_types_order 74 | - print_to_echo 75 | - regular_callable_call 76 | - return_assignment 77 | - self_accessor 78 | - self_static_accessor 79 | - set_type_to_cast 80 | - short_array_syntax 81 | - short_list_syntax 82 | - simplified_if_return 83 | - single_quote 84 | - standardize_not_equals 85 | - ternary_to_null_coalescing 86 | - trailing_comma_in_multiline_array 87 | - unalign_double_arrow 88 | - unalign_equals 89 | -------------------------------------------------------------------------------- /src/Validators/Client/UrlValidator.php: -------------------------------------------------------------------------------- 1 | enableIDN) { 30 | PunycodeAsset::register($view); 31 | } 32 | 33 | $options = $this->getClientOptions($validator, $model, $attribute); 34 | 35 | return 'yii.validation.url(value, messages, ' . Json::htmlEncode($options) . ');'; 36 | } 37 | 38 | /** 39 | * Returns the client-side validation options. 40 | * 41 | * @param \yii\validators\UrlValidator $validator the server-side validator. 42 | * @param \yii\base\Model $model the model being validated 43 | * @param string $attribute the attribute name being validated 44 | * 45 | * @return array the client-side validation options 46 | */ 47 | public function getClientOptions($validator, $model, $attribute) 48 | { 49 | if (strpos($validator->pattern, '{schemes}') !== false) { 50 | $pattern = str_replace('{schemes}', '(' . implode('|', $validator->validSchemes) . ')', $validator->pattern); 51 | } else { 52 | $pattern = $validator->pattern; 53 | } 54 | 55 | $options = [ 56 | 'pattern' => new JsExpression($pattern), 57 | 'message' => $validator->formatMessage($validator->message, [ 58 | 'attribute' => $model->getAttributeLabel($attribute), 59 | ]), 60 | 'enableIDN' => (bool) $validator->enableIDN, 61 | ]; 62 | 63 | if ($validator->skipOnEmpty) { 64 | $options['skipOnEmpty'] = 1; 65 | } 66 | 67 | if ($validator->defaultScheme !== null) { 68 | $options['defaultScheme'] = $validator->defaultScheme; 69 | } 70 | 71 | return $options; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/yii-jquery", 3 | "type": "library", 4 | "description": "Yii Framework jQuery Extension", 5 | "keywords": [ 6 | "yii", 7 | "jquery", 8 | "javascript", 9 | "client script" 10 | ], 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "source": "https://github.com/yiisoft/yii-jquery", 14 | "issues": "https://github.com/yiisoft/yii-jquery/issues", 15 | "forum": "https://www.yiiframework.com/forum/", 16 | "wiki": "https://www.yiiframework.com/wiki/", 17 | "irc": "irc://irc.freenode.net/yii" 18 | }, 19 | "minimum-stability": "dev", 20 | "prefer-stable": true, 21 | "require": { 22 | "php": "^7.4|^8.0", 23 | "nyholm/psr7": "^1.0", 24 | "psr/event-dispatcher": "1.0.0", 25 | "yiisoft/assets": "^2.0", 26 | "yiisoft/html": "^2.0" 27 | }, 28 | "require-dev": { 29 | "foxy/foxy": "^1.0", 30 | "phan/phan": "^3.0", 31 | "phpunit/phpunit": "^9.4", 32 | "roave/infection-static-analysis-plugin": "^1.5", 33 | "spatie/phpunit-watcher": "^1.23", 34 | "vimeo/psalm": "^4.2", 35 | "yiisoft/cache": "^1.0", 36 | "yiisoft/composer-config-plugin": "^1.0@dev", 37 | "yiisoft/di": "^1.0", 38 | "yiisoft/event-dispatcher": "^1.0", 39 | "yiisoft/log": "^1.0", 40 | "yiisoft/yii-dataview": "^3.0@dev" 41 | }, 42 | "repositories": [ 43 | { 44 | "type": "composer", 45 | "url": "https://asset-packagist.org" 46 | } 47 | ], 48 | "autoload": { 49 | "psr-4": { 50 | "Yiisoft\\Yii\\JQuery\\": "src" 51 | } 52 | }, 53 | "autoload-dev": { 54 | "psr-4": { 55 | "Yiisoft\\Yii\\JQuery\\Tests\\": "tests" 56 | } 57 | }, 58 | "extra": { 59 | "branch-alias": { 60 | "dev-master": "3.0.x-dev" 61 | }, 62 | "config-plugin-options": { 63 | "source-directory": "config" 64 | }, 65 | "config-plugin": { 66 | "tests": "tests.php" 67 | } 68 | }, 69 | "config": { 70 | "sort-packages": true, 71 | "allow-plugins": { 72 | "infection/extension-installer": true, 73 | "composer/package-versions-deprecated": true 74 | } 75 | }, 76 | "scripts": { 77 | "test": "phpunit --testdox --no-interaction", 78 | "test-watch": "phpunit-watcher watch" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/GridViewClientScript.php: -------------------------------------------------------------------------------- 1 | dataProvider($dataProvider) 22 | * ->asClientScript([ 23 | * 'class' => Yiisoft\Yii\JQuery\GridViewClientScript::class 24 | * ]) 25 | * ->columns([ 26 | * 'id', 27 | * 'name', 28 | * 'created_at:datetime', 29 | * // ... 30 | * ]); ?> 31 | * ``` 32 | * 33 | * @see \Yiisoft\Yii\DataView\GridView 34 | * @see GridViewAsset 35 | * 36 | * @property \Yiisoft\Yii\DataView\GridView $owner the owner of this behavior. 37 | */ 38 | class GridViewClientScript extends Behavior 39 | { 40 | /** 41 | * @var string additional jQuery selector for selecting filter input fields. 42 | */ 43 | public $filterSelector; 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function events() 49 | { 50 | return [ 51 | RunEvent::BEFORE => 'beforeRun', 52 | ]; 53 | } 54 | 55 | /** 56 | * Handles {@see \Yiisoft\Widget\Widget::EVENT_BEFORE_RUN} event, registering related client script. 57 | * 58 | * @param \Yiisoft\Widget\Event\BeforeRun $event event instance. 59 | */ 60 | public function beforeRun(BeforeRun $event) 61 | { 62 | $id = $this->owner->options['id']; 63 | $options = Json::htmlEncode($this->getClientOptions()); 64 | $view = $this->owner->getView(); 65 | 66 | GridViewAsset::register($view); 67 | $view->registerJs("jQuery('#$id').yiiGridView($options);"); 68 | } 69 | 70 | /** 71 | * Returns the options for the grid view JS widget. 72 | * 73 | * @return array the options 74 | */ 75 | protected function getClientOptions() 76 | { 77 | $filterUrl = $this->owner->filterUrl ?? Yii::getApp()->request->url; 78 | $id = $this->owner->filterRowOptions['id']; 79 | $filterSelector = "#$id input, #$id select"; 80 | if (isset($this->filterSelector)) { 81 | $filterSelector .= ', ' . $this->filterSelector; 82 | } 83 | 84 | return [ 85 | 'filterUrl' => Url::to($filterUrl), 86 | 'filterSelector' => $filterSelector, 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Validators/Client/CompareValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 26 | 27 | return 'yii.validation.compare(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 28 | } 29 | 30 | /** 31 | * Returns the client-side validation options. 32 | * 33 | * @param \yii\validators\CompareValidator $validator the server-side validator. 34 | * @param \yii\base\Model $model the model being validated 35 | * @param string $attribute the attribute name being validated 36 | * 37 | * @return array the client-side validation options 38 | */ 39 | public function getClientOptions($validator, $model, $attribute) 40 | { 41 | $options = [ 42 | 'operator' => $validator->operator, 43 | 'type' => $validator->type, 44 | ]; 45 | 46 | if ($validator->compareValue !== null) { 47 | $options['compareValue'] = $validator->compareValue; 48 | $compareLabel = $compareValue = $compareValueOrAttribute = $validator->compareValue; 49 | } else { 50 | $compareAttribute = $validator->compareAttribute === null ? $attribute . '_repeat' : $validator->compareAttribute; 51 | $compareValue = $model->getAttributeLabel($compareAttribute); 52 | $options['compareAttribute'] = Html::getInputId($model, $compareAttribute); 53 | $compareLabel = $compareValueOrAttribute = $model->getAttributeLabel($compareAttribute); 54 | } 55 | 56 | if ($validator->skipOnEmpty) { 57 | $options['skipOnEmpty'] = 1; 58 | } 59 | 60 | $options['message'] = $validator->formatMessage($validator->message, [ 61 | 'attribute' => $model->getAttributeLabel($attribute), 62 | 'compareAttribute' => $compareLabel, 63 | 'compareValue' => $compareValue, 64 | 'compareValueOrAttribute' => $compareValueOrAttribute, 65 | ]); 66 | 67 | return $options; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Validators/Client/StringValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 25 | 26 | return 'yii.validation.string(value, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ');'; 27 | } 28 | 29 | /** 30 | * Returns the client-side validation options. 31 | * 32 | * @param \yii\validators\StringValidator $validator the server-side validator. 33 | * @param \yii\base\Model $model the model being validated 34 | * @param string $attribute the attribute name being validated 35 | * 36 | * @return array the client-side validation options 37 | */ 38 | public function getClientOptions($validator, $model, $attribute) 39 | { 40 | $label = $model->getAttributeLabel($attribute); 41 | 42 | $options = [ 43 | 'message' => $validator->formatMessage($validator->message, [ 44 | 'attribute' => $label, 45 | ]), 46 | ]; 47 | 48 | if ($validator->min !== null) { 49 | $options['min'] = $validator->min; 50 | $options['tooShort'] = $validator->formatMessage($validator->tooShort, [ 51 | 'attribute' => $label, 52 | 'min' => $validator->min, 53 | ]); 54 | } 55 | 56 | if ($validator->max !== null) { 57 | $options['max'] = $validator->max; 58 | $options['tooLong'] = $validator->formatMessage($validator->tooLong, [ 59 | 'attribute' => $label, 60 | 'max' => $validator->max, 61 | ]); 62 | } 63 | 64 | if ($validator->length !== null) { 65 | $options['is'] = $validator->length; 66 | $options['notEqual'] = $validator->formatMessage($validator->notEqual, [ 67 | 'attribute' => $label, 68 | 'length' => $validator->length, 69 | ]); 70 | } 71 | 72 | if ($validator->skipOnEmpty) { 73 | $options['skipOnEmpty'] = 1; 74 | } 75 | 76 | return $options; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Validators/Client/IpValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 28 | 29 | return 'yii.validation.ip(value, messages, ' . Json::htmlEncode($options) . ');'; 30 | } 31 | 32 | /** 33 | * Returns the client-side validation options. 34 | * 35 | * @param \yii\validators\IpValidator $validator the server-side validator. 36 | * @param \yii\base\Model $model the model being validated 37 | * @param string $attribute the attribute name being validated 38 | * 39 | * @return array the client-side validation options 40 | */ 41 | public function getClientOptions($validator, $model, $attribute) 42 | { 43 | $messages = [ 44 | 'ipv6NotAllowed' => $validator->ipv6NotAllowed, 45 | 'ipv4NotAllowed' => $validator->ipv4NotAllowed, 46 | 'message' => $validator->message, 47 | 'noSubnet' => $validator->noSubnet, 48 | 'hasSubnet' => $validator->hasSubnet, 49 | ]; 50 | 51 | foreach ($messages as &$message) { 52 | $message = $validator->formatMessage($message, [ 53 | 'attribute' => $model->getAttributeLabel($attribute), 54 | ]); 55 | } 56 | 57 | $options = [ 58 | 'ipv4Pattern' => new JsExpression(Html::escapeJsRegularExpression($validator->ipv4Pattern)), 59 | 'ipv6Pattern' => new JsExpression(Html::escapeJsRegularExpression($validator->ipv6Pattern)), 60 | 'messages' => $messages, 61 | 'ipv4' => (bool) $validator->ipv4, 62 | 'ipv6' => (bool) $validator->ipv6, 63 | 'ipParsePattern' => new JsExpression(Html::escapeJsRegularExpression($validator->getIpParsePattern())), 64 | 'negation' => $validator->negation, 65 | 'subnet' => $validator->subnet, 66 | ]; 67 | 68 | if ($validator->skipOnEmpty) { 69 | $options['skipOnEmpty'] = 1; 70 | } 71 | 72 | return $options; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Validators/Client/NumberValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 27 | 28 | return 'yii.validation.number(value, messages, ' . Json::htmlEncode($options) . ');'; 29 | } 30 | 31 | /** 32 | * Returns the client-side validation options. 33 | * 34 | * @param \yii\validators\NumberValidator $validator the server-side validator. 35 | * @param \yii\base\Model $model the model being validated 36 | * @param string $attribute the attribute name being validated 37 | * 38 | * @return array the client-side validation options 39 | */ 40 | public function getClientOptions($validator, $model, $attribute) 41 | { 42 | $label = $model->getAttributeLabel($attribute); 43 | 44 | $options = [ 45 | 'pattern' => new JsExpression($validator->integerOnly ? $validator->integerPattern : $validator->numberPattern), 46 | 'message' => $validator->formatMessage($validator->message, [ 47 | 'attribute' => $label, 48 | ]), 49 | ]; 50 | 51 | if ($validator->min !== null) { 52 | // ensure numeric value to make javascript comparison equal to PHP comparison 53 | // https://github.com/yiisoft/yii2/issues/3118 54 | $options['min'] = is_string($validator->min) ? (float) $validator->min : $validator->min; 55 | $options['tooSmall'] = $validator->formatMessage($validator->tooSmall, [ 56 | 'attribute' => $label, 57 | 'min' => $validator->min, 58 | ]); 59 | } 60 | 61 | if ($validator->max !== null) { 62 | // ensure numeric value to make javascript comparison equal to PHP comparison 63 | // https://github.com/yiisoft/yii2/issues/3118 64 | $options['max'] = is_string($validator->max) ? (float) $validator->max : $validator->max; 65 | $options['tooBig'] = $validator->formatMessage($validator->tooBig, [ 66 | 'attribute' => $label, 67 | 'max' => $validator->max, 68 | ]); 69 | } 70 | 71 | if ($validator->skipOnEmpty) { 72 | $options['skipOnEmpty'] = 1; 73 | } 74 | 75 | return $options; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Validators/Client/ImageValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 24 | 25 | return 'yii.validation.image(attribute, messages, ' . json_encode($options, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . ', deferred);'; 26 | } 27 | 28 | /** 29 | * Returns the client-side validation options. 30 | * 31 | * @param \yii\validators\ImageValidator $validator the server-side validator. 32 | * @param \yii\base\Model $model the model being validated 33 | * @param string $attribute the attribute name being validated 34 | * 35 | * @return array the client-side validation options 36 | */ 37 | public function getClientOptions($validator, $model, $attribute) 38 | { 39 | $options = parent::getClientOptions($validator, $model, $attribute); 40 | 41 | $label = $model->getAttributeLabel($attribute); 42 | 43 | if ($validator->notImage !== null) { 44 | $options['notImage'] = $validator->formatMessage($validator->notImage, [ 45 | 'attribute' => $label, 46 | ]); 47 | } 48 | 49 | if ($validator->minWidth !== null) { 50 | $options['minWidth'] = $validator->minWidth; 51 | $options['underWidth'] = $validator->formatMessage($validator->underWidth, [ 52 | 'attribute' => $label, 53 | 'limit' => $validator->minWidth, 54 | ]); 55 | } 56 | 57 | if ($validator->maxWidth !== null) { 58 | $options['maxWidth'] = $validator->maxWidth; 59 | $options['overWidth'] = $validator->formatMessage($validator->overWidth, [ 60 | 'attribute' => $label, 61 | 'limit' => $validator->maxWidth, 62 | ]); 63 | } 64 | 65 | if ($validator->minHeight !== null) { 66 | $options['minHeight'] = $validator->minHeight; 67 | $options['underHeight'] = $validator->formatMessage($validator->underHeight, [ 68 | 'attribute' => $label, 69 | 'limit' => $validator->minHeight, 70 | ]); 71 | } 72 | 73 | if ($validator->maxHeight !== null) { 74 | $options['maxHeight'] = $validator->maxHeight; 75 | $options['overHeight'] = $validator->formatMessage($validator->overHeight, [ 76 | 'attribute' => $label, 77 | 'limit' => $validator->maxHeight, 78 | ]); 79 | } 80 | 81 | return $options; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | env: 4 | global: 5 | - DEFAULT_COMPOSER_FLAGS="--prefer-dist --no-interaction --no-progress --optimize-autoloader" 6 | - TASK_STATIC_ANALYSIS=0 7 | - TASK_TESTS_COVERAGE=0 8 | 9 | matrix: 10 | include: 11 | - php: "7.4" 12 | env: 13 | - TASK_STATIC_ANALYSIS=0 # set to 1 to enable static analysis 14 | - TASK_TESTS_COVERAGE=1 15 | 16 | # faster builds on new travis setup not using sudo 17 | sudo: false 18 | 19 | # cache vendor dirs 20 | cache: 21 | directories: 22 | - $HOME/.composer/cache 23 | 24 | before_install: 25 | - | 26 | if [[ $TASK_TESTS_COVERAGE != 1 ]]; then 27 | # disable xdebug for performance reasons when code coverage is not needed 28 | phpenv config-rm xdebug.ini || echo "xdebug is not installed" 29 | fi 30 | 31 | install: 32 | - travis_retry composer self-update && composer --version 33 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 34 | - travis_retry composer install $DEFAULT_COMPOSER_FLAGS 35 | - | 36 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then 37 | pecl install ast 38 | fi 39 | 40 | before_script: 41 | - php --version 42 | - composer --version 43 | # enable code coverage 44 | - | 45 | if [ $TASK_TESTS_COVERAGE == 1 ]; then 46 | PHPUNIT_COVERAGE_FLAG="--coverage-clover=coverage.clover" 47 | fi 48 | 49 | script: 50 | - vendor/bin/phpunit --verbose $PHPUNIT_COVERAGE_FLAG 51 | - | 52 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then 53 | composer phan 54 | fi 55 | - | 56 | if [ $TASK_STATIC_ANALYSIS == 1 ]; then 57 | cat analysis.txt 58 | fi 59 | 60 | after_script: 61 | - | 62 | if [ $TASK_TESTS_COVERAGE == 1 ]; then 63 | travis_retry wget https://scrutinizer-ci.com/ocular.phar 64 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover 65 | fi 66 | 67 | # It should be generated for each repository 68 | # See https://github.com/yiisoft/yii-dev-tool/issues/14#issuecomment-561390952 69 | notifications: 70 | slack: 71 | - 72 | rooms: 73 | - 74 | secure: Qvx4x+h+s8HpOJRYElH5AG3csNFIsqUCcI8KTTflpP+LNDc6R4zhnV2538TrXNM4yZUT8e16x9wqurDs2/MNJOjgAUDhVlV9N2V/yDYEqyw3mvGr9MNIzDxRLlfpJugONhkO4+TL1go8FnBrXwCDpTvLxBo4HusbcSQYQxa6XtP6/KSfFmXh3feydRE95NKEUCJgIU9BPw9Mif99FrYBQGofsa78dR89tW55rfdNrz3daVOnH1iOyU7t+J9sx8JBklKOPawmKGttahbUxuChagiMdC8Onu1kEigo1FGKrif6o0Us2PXFoWvbMLhW4zgvKwSiCozB0FVIKRTBSM5VFGhRHxHaL3v82gicTxdfNyAG5TVAwgpoQoVQt6U27oFNU5LIpnqLM5mK4kDX/IExw/EtuQHZCNhXcM41PU0NJoBgLDyn0/dsPZsVSsQ5cwFvTkzMzXggHxnT2Zbh/HvbZiUfhGIf1FUrmd8XbBBsH2A7/Vt2FaC9EUTKQU5ii6/D84tgp2c0qvp7DjtDd9rrzXDpEdFdYqDcGmbFu5zr6hw2z6LFizIGmrkMNgJVaN1CH/R1Islps0Nm23dLZewjgHNIboR97duZrgCt3gBOx/gzl9tA8iL8kRMT9TU3WnD7Kv49mvyfsfVSVDds5nyle+FzVDxzj6g2g/Kq3AQBMPQ= 75 | on_success: always 76 | on_failure: never 77 | on_pull_requests: false 78 | - 79 | rooms: 80 | - 81 | secure: L9bTpHxLU5go+gBbAMdvyF0HZYij7G9ifn+JOtvPFkMavi5xl81d3QdWD6nc/67WSrhgdYJ8ApAwrisvAAyaNYwFBP1VC0BkwVcCXOQLpQKuS+hMankQNHauk3bdfclBU6UTLA8o6Yil7nF+NaUb/j3ElVwjHgiE5IQrHpPOWt3B9VeEFw64LX/oEfoRYYNbkMXMehRkKeWmgthLQ5MR+9KCutUVAWZlKuVFYoTyiXdYq6uk17V42/9XmPuVShY4VTdPfxUAQyXn16+usMfD2K5a75cAGKSSCgqgnDd5OzHh2D2JW6HyFl1RWAVcOrzUda+CUfQQPZbJFRg40m6Rf12YQMri4uszdSlyNEYXwRgxT7MvxNCOX/uwWyIb2D0d1qYs85qkkBIFrS7DBkx57GX3cNQnxUp2+ARvZnmsHlA10csJP0mq6jaJzPAR1mJuh9z26x9LEZ4fki07Rr1W1nvjwvizMDudBRhp3F+rzlWeq2/bX52twJ4pJhYFT6wLI5xjdufgyu9+tjCHPenN0EqeFMpaKxxBuKjfBRAgNZfeh6WP7FA/PurgHh4JvVGj6eDBqU5ZsE5ZguqZ9IzDHbL4EoFOrmzjiXmJkD0ZYIFlwhL841QILTn7R6A708N6ShAuWcJPyrnuPSWABs9sX3xVDXHDZI2OsYge3A5dI6U= 82 | on_success: never 83 | on_failure: always 84 | on_pull_requests: false 85 | -------------------------------------------------------------------------------- /src/ActiveFormClientScript.php: -------------------------------------------------------------------------------- 1 | options(['id' => 'example-form') 19 | * ->asClientScript(\Yiisoft\Yii\JQuery\ActiveFormClientScript::class); ?> 20 | * ... 21 | * 22 | * ``` 23 | */ 24 | class ActiveFormClientScript extends \Yiisoft\Widgets\ActiveFormClientScript 25 | { 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | protected function defaultClientValidatorMap() 30 | { 31 | return [ 32 | \Yiisoft\Validator\Rule\Boolean::class => \Yiisoft\Yii\JQuery\Validators\Client\BooleanValidator::class, 33 | \Yiisoft\Validator\Rule\CompareTo::class => \Yiisoft\Yii\JQuery\Validators\Client\CompareValidator::class, 34 | \Yiisoft\Validator\Rule\Email::class => \Yiisoft\Yii\JQuery\Validators\Client\EmailValidator::class, 35 | \Yiisoft\Validator\Rule\Ip::class => \Yiisoft\Yii\JQuery\Validators\Client\IpValidator::class, 36 | \Yiisoft\Validator\Rule\Number::class => \Yiisoft\Yii\JQuery\Validators\Client\NumberValidator::class, 37 | \Yiisoft\Validator\Rule\Required::class => \Yiisoft\Yii\JQuery\Validators\Client\RequiredValidator::class, 38 | \Yiisoft\Validator\Rule\Url::class => \Yiisoft\Yii\JQuery\Validators\Client\UrlValidator::class, 39 | ]; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | protected function registerClientScript() 46 | { 47 | $id = $this->owner->options['id']; 48 | $options = Json::htmlEncode($this->getClientOptions()); 49 | $attributes = Json::htmlEncode($this->attributes); 50 | 51 | ActiveFormAsset::register($view); 52 | 53 | $view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | protected function getFieldClientOptions($field) 60 | { 61 | $options = parent::getFieldClientOptions($field); 62 | 63 | // only get the options that are different from the default ones (set in yii.activeForm.js) 64 | return array_diff_assoc($options, [ 65 | 'validateOnChange' => true, 66 | 'validateOnBlur' => true, 67 | 'validateOnType' => false, 68 | 'validationDelay' => 500, 69 | 'encodeError' => true, 70 | 'error' => '.help-block', 71 | 'updateAriaInvalid' => true, 72 | ]); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | protected function getClientOptions() 79 | { 80 | $options = parent::getClientOptions(); 81 | 82 | // only get the options that are different from the default ones (set in yii.activeForm.js) 83 | return array_diff_assoc($options, [ 84 | 'encodeErrorSummary' => true, 85 | 'errorSummary' => '.error-summary', 86 | 'validateOnSubmit' => true, 87 | 'errorCssClass' => 'has-error', 88 | 'successCssClass' => 'has-success', 89 | 'validatingCssClass' => 'validating', 90 | 'ajaxParam' => 'ajax', 91 | 'ajaxDataType' => 'json', 92 | 'scrollToError' => true, 93 | 'scrollToErrorOffset' => 0, 94 | 'validationStateOn' => ActiveForm::VALIDATION_STATE_ON_CONTAINER, 95 | ]); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Validators/Client/FileValidator.php: -------------------------------------------------------------------------------- 1 | getClientOptions($validator, $model, $attribute); 28 | 29 | return 'yii.validation.file(attribute, messages, ' . Json::encode($options) . ');'; 30 | } 31 | 32 | /** 33 | * Returns the client-side validation options. 34 | * 35 | * @param \yii\validators\FileValidator $validator the server-side validator. 36 | * @param \yii\base\Model $model the model being validated 37 | * @param string $attribute the attribute name being validated 38 | * 39 | * @return array the client-side validation options 40 | */ 41 | public function getClientOptions($validator, $model, $attribute) 42 | { 43 | $label = $model->getAttributeLabel($attribute); 44 | 45 | $options = []; 46 | 47 | if ($validator->message !== null) { 48 | $options['message'] = $validator->formatMessage($validator->message, [ 49 | 'attribute' => $label, 50 | ]); 51 | } 52 | 53 | $options['skipOnEmpty'] = $validator->skipOnEmpty; 54 | 55 | if (!$validator->skipOnEmpty) { 56 | $options['uploadRequired'] = $validator->formatMessage($validator->uploadRequired, [ 57 | 'attribute' => $label, 58 | ]); 59 | } 60 | 61 | if ($validator->mimeTypes !== null) { 62 | $mimeTypes = []; 63 | foreach ($validator->mimeTypes as $mimeType) { 64 | $mimeTypes[] = new JsExpression(Html::escapeJsRegularExpression($validator->buildMimeTypeRegexp($mimeType))); 65 | } 66 | $options['mimeTypes'] = $mimeTypes; 67 | $options['wrongMimeType'] = $validator->formatMessage($validator->wrongMimeType, [ 68 | 'attribute' => $label, 69 | 'mimeTypes' => implode(', ', $validator->mimeTypes), 70 | ]); 71 | } 72 | 73 | if ($validator->extensions !== null) { 74 | $options['extensions'] = $validator->extensions; 75 | $options['wrongExtension'] = $validator->formatMessage($validator->wrongExtension, [ 76 | 'attribute' => $label, 77 | 'extensions' => implode(', ', $validator->extensions), 78 | ]); 79 | } 80 | 81 | if ($validator->minSize !== null) { 82 | $options['minSize'] = $validator->minSize; 83 | $options['tooSmall'] = $validator->formatMessage($validator->tooSmall, [ 84 | 'attribute' => $label, 85 | 'limit' => $validator->minSize, 86 | 'formattedLimit' => Yii::$app->formatter->asShortSize($validator->minSize), 87 | ]); 88 | } 89 | 90 | if ($validator->maxSize !== null) { 91 | $options['maxSize'] = $validator->maxSize; 92 | $options['tooBig'] = $validator->formatMessage($validator->tooBig, [ 93 | 'attribute' => $label, 94 | 'limit' => $validator->getSizeLimit(), 95 | 'formattedLimit' => Yii::$app->formatter->asShortSize($validator->getSizeLimit()), 96 | ]); 97 | } 98 | 99 | if ($validator->maxFiles !== null) { 100 | $options['maxFiles'] = $validator->maxFiles; 101 | $options['tooMany'] = $validator->formatMessage($validator->tooMany, [ 102 | 'attribute' => $label, 103 | 'limit' => $validator->maxFiles, 104 | ]); 105 | } 106 | 107 | return $options; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/assets/yii.gridView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Yii GridView widget. 3 | * 4 | * This is the JavaScript widget used by the yii\grid\GridView widget. 5 | */ 6 | (function ($) { 7 | $.fn.yiiGridView = function (method) { 8 | if (methods[method]) { 9 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 10 | } else if (typeof method === 'object' || !method) { 11 | return methods.init.apply(this, arguments); 12 | } else { 13 | $.error('Method ' + method + ' does not exist in jQuery.yiiGridView'); 14 | return false; 15 | } 16 | }; 17 | 18 | var defaults = { 19 | filterUrl: undefined, 20 | filterSelector: undefined 21 | }; 22 | 23 | var gridData = {}; 24 | 25 | var gridEvents = { 26 | /** 27 | * beforeFilter event is triggered before filtering the grid. 28 | * The signature of the event handler should be: 29 | * function (event) 30 | * where 31 | * - event: an Event object. 32 | * 33 | * If the handler returns a boolean false, it will stop filter form submission after this event. As 34 | * a result, afterFilter event will not be triggered. 35 | */ 36 | beforeFilter: 'beforeFilter', 37 | /** 38 | * afterFilter event is triggered after filtering the grid and filtered results are fetched. 39 | * The signature of the event handler should be: 40 | * function (event) 41 | * where 42 | * - event: an Event object. 43 | */ 44 | afterFilter: 'afterFilter' 45 | }; 46 | 47 | /** 48 | * Used for storing active event handlers and removing them later. 49 | * The structure of single event handler is: 50 | * 51 | * { 52 | * gridViewId: { 53 | * type: { 54 | * event: '...', 55 | * selector: '...' 56 | * } 57 | * } 58 | * } 59 | * 60 | * Used types: 61 | * 62 | * - filter, used for filtering grid with elements found by filterSelector 63 | * - checkRow, used for checking single row 64 | * - checkAllRows, used for checking all rows with according "Check all" checkbox 65 | * 66 | * event is the name of event, for example: 'change.yiiGridView' 67 | * selector is a jQuery selector for finding elements 68 | * 69 | * @type {{}} 70 | */ 71 | var gridEventHandlers = {}; 72 | 73 | var methods = { 74 | init: function (options) { 75 | return this.each(function () { 76 | var $e = $(this); 77 | var settings = $.extend({}, defaults, options || {}); 78 | var id = $e.attr('id'); 79 | if (gridData[id] === undefined) { 80 | gridData[id] = {}; 81 | } 82 | 83 | gridData[id] = $.extend(gridData[id], {settings: settings}); 84 | 85 | var filterEvents = 'change.yiiGridView keydown.yiiGridView'; 86 | var enterPressed = false; 87 | initEventHandler($e, 'filter', filterEvents, settings.filterSelector, function (event) { 88 | if (event.type === 'keydown') { 89 | if (event.keyCode !== 13) { 90 | return; // only react to enter key 91 | } else { 92 | enterPressed = true; 93 | } 94 | } else { 95 | // prevent processing for both keydown and change events 96 | if (enterPressed) { 97 | enterPressed = false; 98 | return; 99 | } 100 | } 101 | 102 | methods.applyFilter.apply($e); 103 | 104 | return false; 105 | }); 106 | }); 107 | }, 108 | 109 | applyFilter: function () { 110 | var $grid = $(this); 111 | var settings = gridData[$grid.attr('id')].settings; 112 | var data = {}; 113 | $.each($(settings.filterSelector).serializeArray(), function () { 114 | if (!(this.name in data)) { 115 | data[this.name] = []; 116 | } 117 | data[this.name].push(this.value); 118 | }); 119 | 120 | var namesInFilter = Object.keys(data); 121 | 122 | $.each(yii.getQueryParams(settings.filterUrl), function (name, value) { 123 | if (namesInFilter.indexOf(name) === -1 && namesInFilter.indexOf(name.replace(/\[\d*\]$/, '')) === -1) { 124 | if (!$.isArray(value)) { 125 | value = [value]; 126 | } 127 | if (!(name in data)) { 128 | data[name] = value; 129 | } else { 130 | $.each(value, function (i, val) { 131 | if ($.inArray(val, data[name])) { 132 | data[name].push(val); 133 | } 134 | }); 135 | } 136 | } 137 | }); 138 | 139 | var pos = settings.filterUrl.indexOf('?'); 140 | var url = pos < 0 ? settings.filterUrl : settings.filterUrl.substring(0, pos); 141 | var hashPos = settings.filterUrl.indexOf('#'); 142 | if (pos >= 0 && hashPos >= 0) { 143 | url += settings.filterUrl.substring(hashPos); 144 | } 145 | 146 | $grid.find('form.gridview-filter-form').remove(); 147 | var $form = $('
', { 148 | action: url, 149 | method: 'get', 150 | 'class': 'gridview-filter-form', 151 | style: 'display:none' 152 | }).appendTo($grid); 153 | $.each(data, function (name, values) { 154 | $.each(values, function (index, value) { 155 | $form.append($('').attr({type: 'hidden', name: name, value: value})); 156 | }); 157 | }); 158 | 159 | var event = $.Event(gridEvents.beforeFilter); 160 | $grid.trigger(event); 161 | if (event.result === false) { 162 | return; 163 | } 164 | 165 | $form.submit(); 166 | 167 | $grid.trigger(gridEvents.afterFilter); 168 | }, 169 | 170 | setSelectionColumn: function (options) { 171 | var $grid = $(this); 172 | var id = $(this).attr('id'); 173 | if (gridData[id] === undefined) { 174 | gridData[id] = {}; 175 | } 176 | gridData[id].selectionColumn = options.name; 177 | if (!options.multiple || !options.checkAll) { 178 | return; 179 | } 180 | var checkAll = "#" + id + " input[name='" + options.checkAll + "']"; 181 | var inputs = options['class'] ? "input." + options['class'] : "input[name='" + options.name + "']"; 182 | var inputsEnabled = "#" + id + " " + inputs + ":enabled"; 183 | initEventHandler($grid, 'checkAllRows', 'click.yiiGridView', checkAll, function () { 184 | $grid.find(inputs + ":enabled").prop('checked', this.checked); 185 | }); 186 | initEventHandler($grid, 'checkRow', 'click.yiiGridView', inputsEnabled, function () { 187 | var all = $grid.find(inputs).length == $grid.find(inputs + ":checked").length; 188 | $grid.find("input[name='" + options.checkAll + "']").prop('checked', all); 189 | }); 190 | }, 191 | 192 | getSelectedRows: function () { 193 | var $grid = $(this); 194 | var data = gridData[$grid.attr('id')]; 195 | var keys = []; 196 | if (data.selectionColumn) { 197 | $grid.find("input[name='" + data.selectionColumn + "']:checked").each(function () { 198 | keys.push($(this).parent().closest('tr').data('key')); 199 | }); 200 | } 201 | 202 | return keys; 203 | }, 204 | 205 | destroy: function () { 206 | var events = ['.yiiGridView', gridEvents.beforeFilter, gridEvents.afterFilter].join(' '); 207 | this.off(events); 208 | 209 | var id = $(this).attr('id'); 210 | $.each(gridEventHandlers[id], function (type, data) { 211 | $(document).off(data.event, data.selector); 212 | }); 213 | 214 | delete gridData[id]; 215 | 216 | return this; 217 | }, 218 | 219 | data: function () { 220 | var id = $(this).attr('id'); 221 | return gridData[id]; 222 | } 223 | }; 224 | 225 | /** 226 | * Used for attaching event handler and prevent of duplicating them. With each call previously attached handler of 227 | * the same type is removed even selector was changed. 228 | * @param {jQuery} $gridView According jQuery grid view element 229 | * @param {string} type Type of the event which acts like a key 230 | * @param {string} event Event name, for example 'change.yiiGridView' 231 | * @param {string} selector jQuery selector 232 | * @param {function} callback The actual function to be executed with this event 233 | */ 234 | function initEventHandler($gridView, type, event, selector, callback) { 235 | var id = $gridView.attr('id'); 236 | var prevHandler = gridEventHandlers[id]; 237 | if (prevHandler !== undefined && prevHandler[type] !== undefined) { 238 | var data = prevHandler[type]; 239 | $(document).off(data.event, data.selector); 240 | } 241 | if (prevHandler === undefined) { 242 | gridEventHandlers[id] = {}; 243 | } 244 | $(document).on(event, selector, callback); 245 | gridEventHandlers[id][type] = {event: event, selector: selector}; 246 | } 247 | })(window.jQuery); 248 | -------------------------------------------------------------------------------- /src/assets/yii.validation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Yii validation module. 3 | * 4 | * This JavaScript module provides the validation methods for the built-in validators. 5 | */ 6 | 7 | yii.validation = (function ($) { 8 | var pub = { 9 | isEmpty: function (value) { 10 | return value === null || value === undefined || ($.isArray(value) && value.length === 0) || value === ''; 11 | }, 12 | 13 | addMessage: function (messages, message, value) { 14 | messages.push(message.replace(/\{value\}/g, value)); 15 | }, 16 | 17 | required: function (value, messages, options) { 18 | var valid = false; 19 | if (options.requiredValue === undefined) { 20 | var isString = typeof value == 'string' || value instanceof String; 21 | if (options.strict && value !== undefined || !options.strict && !pub.isEmpty(isString ? $.trim(value) : value)) { 22 | valid = true; 23 | } 24 | } else if (!options.strict && value == options.requiredValue || options.strict && value === options.requiredValue) { 25 | valid = true; 26 | } 27 | 28 | if (!valid) { 29 | pub.addMessage(messages, options.message, value); 30 | } 31 | }, 32 | 33 | // "boolean" is a reserved keyword in older versions of ES so it's quoted for IE < 9 support 34 | 'boolean': function (value, messages, options) { 35 | if (options.skipOnEmpty && pub.isEmpty(value)) { 36 | return; 37 | } 38 | var valid = !options.strict && (value == options.trueValue || value == options.falseValue) 39 | || options.strict && (value === options.trueValue || value === options.falseValue); 40 | 41 | if (!valid) { 42 | pub.addMessage(messages, options.message, value); 43 | } 44 | }, 45 | 46 | string: function (value, messages, options) { 47 | if (options.skipOnEmpty && pub.isEmpty(value)) { 48 | return; 49 | } 50 | 51 | if (typeof value !== 'string') { 52 | pub.addMessage(messages, options.message, value); 53 | return; 54 | } 55 | 56 | if (options.is !== undefined && value.length != options.is) { 57 | pub.addMessage(messages, options.notEqual, value); 58 | return; 59 | } 60 | if (options.min !== undefined && value.length < options.min) { 61 | pub.addMessage(messages, options.tooShort, value); 62 | } 63 | if (options.max !== undefined && value.length > options.max) { 64 | pub.addMessage(messages, options.tooLong, value); 65 | } 66 | }, 67 | 68 | file: function (attribute, messages, options) { 69 | var files = getUploadedFiles(attribute, messages, options); 70 | $.each(files, function (i, file) { 71 | validateFile(file, messages, options); 72 | }); 73 | }, 74 | 75 | image: function (attribute, messages, options, deferredList) { 76 | var files = getUploadedFiles(attribute, messages, options); 77 | $.each(files, function (i, file) { 78 | validateFile(file, messages, options); 79 | 80 | // Skip image validation if FileReader API is not available 81 | if (typeof FileReader === "undefined") { 82 | return; 83 | } 84 | 85 | var deferred = $.Deferred(); 86 | pub.validateImage(file, messages, options, deferred, new FileReader(), new Image()); 87 | deferredList.push(deferred); 88 | }); 89 | }, 90 | 91 | validateImage: function (file, messages, options, deferred, fileReader, image) { 92 | image.onload = function() { 93 | validateImageSize(file, image, messages, options); 94 | deferred.resolve(); 95 | }; 96 | 97 | image.onerror = function () { 98 | messages.push(options.notImage.replace(/\{file\}/g, file.name)); 99 | deferred.resolve(); 100 | }; 101 | 102 | fileReader.onload = function () { 103 | image.src = this.result; 104 | }; 105 | 106 | // Resolve deferred if there was error while reading data 107 | fileReader.onerror = function () { 108 | deferred.resolve(); 109 | }; 110 | 111 | fileReader.readAsDataURL(file); 112 | }, 113 | 114 | number: function (value, messages, options) { 115 | if (options.skipOnEmpty && pub.isEmpty(value)) { 116 | return; 117 | } 118 | 119 | if (typeof value === 'string' && !options.pattern.test(value)) { 120 | pub.addMessage(messages, options.message, value); 121 | return; 122 | } 123 | 124 | if (options.min !== undefined && value < options.min) { 125 | pub.addMessage(messages, options.tooSmall, value); 126 | } 127 | if (options.max !== undefined && value > options.max) { 128 | pub.addMessage(messages, options.tooBig, value); 129 | } 130 | }, 131 | 132 | range: function (value, messages, options) { 133 | if (options.skipOnEmpty && pub.isEmpty(value)) { 134 | return; 135 | } 136 | 137 | if (!options.allowArray && $.isArray(value)) { 138 | pub.addMessage(messages, options.message, value); 139 | return; 140 | } 141 | 142 | var inArray = true; 143 | 144 | $.each($.isArray(value) ? value : [value], function (i, v) { 145 | if ($.inArray(v, options.range) == -1) { 146 | inArray = false; 147 | return false; 148 | } else { 149 | return true; 150 | } 151 | }); 152 | 153 | if (options.not === undefined) { 154 | options.not = false; 155 | } 156 | 157 | if (options.not === inArray) { 158 | pub.addMessage(messages, options.message, value); 159 | } 160 | }, 161 | 162 | regularExpression: function (value, messages, options) { 163 | if (options.skipOnEmpty && pub.isEmpty(value)) { 164 | return; 165 | } 166 | 167 | if (!options.not && !options.pattern.test(value) || options.not && options.pattern.test(value)) { 168 | pub.addMessage(messages, options.message, value); 169 | } 170 | }, 171 | 172 | email: function (value, messages, options) { 173 | if (options.skipOnEmpty && pub.isEmpty(value)) { 174 | return; 175 | } 176 | 177 | var valid = true, 178 | regexp = /^((?:"?([^"]*)"?\s)?)(?:\s+)?(?:(]+))(>?))$/, 179 | matches = regexp.exec(value); 180 | 181 | if (matches === null) { 182 | valid = false; 183 | } else { 184 | var localPart = matches[5], 185 | domain = matches[6]; 186 | 187 | if (options.enableIDN) { 188 | localPart = punycode.toASCII(localPart); 189 | domain = punycode.toASCII(domain); 190 | 191 | value = matches[1] + matches[3] + localPart + '@' + domain + matches[7]; 192 | } 193 | 194 | if (localPart.length > 64) { 195 | valid = false; 196 | } else if ((localPart + '@' + domain).length > 254) { 197 | valid = false; 198 | } else { 199 | valid = options.pattern.test(value) || (options.allowName && options.fullPattern.test(value)); 200 | } 201 | } 202 | 203 | if (!valid) { 204 | pub.addMessage(messages, options.message, value); 205 | } 206 | }, 207 | 208 | url: function (value, messages, options) { 209 | if (options.skipOnEmpty && pub.isEmpty(value)) { 210 | return; 211 | } 212 | 213 | if (options.defaultScheme && !/:\/\//.test(value)) { 214 | value = options.defaultScheme + '://' + value; 215 | } 216 | 217 | var valid = true; 218 | 219 | if (options.enableIDN) { 220 | var matches = /^([^:]+):\/\/([^\/]+)(.*)$/.exec(value); 221 | if (matches === null) { 222 | valid = false; 223 | } else { 224 | value = matches[1] + '://' + punycode.toASCII(matches[2]) + matches[3]; 225 | } 226 | } 227 | 228 | if (!valid || !options.pattern.test(value)) { 229 | pub.addMessage(messages, options.message, value); 230 | } 231 | }, 232 | 233 | trim: function ($form, attribute, options) { 234 | var $input = $form.find(attribute.input); 235 | var value = $input.val(); 236 | if (!options.skipOnEmpty || !pub.isEmpty(value)) { 237 | value = $.trim(value); 238 | $input.val(value); 239 | } 240 | 241 | return value; 242 | }, 243 | 244 | captcha: function (value, messages, options) { 245 | if (options.skipOnEmpty && pub.isEmpty(value)) { 246 | return; 247 | } 248 | 249 | // CAPTCHA may be updated via AJAX and the updated hash is stored in body data 250 | var hash = $('body').data(options.hashKey); 251 | hash = hash == null ? options.hash : hash[options.caseSensitive ? 0 : 1]; 252 | var v = options.caseSensitive ? value : value.toLowerCase(); 253 | for (var i = v.length - 1, h = 0; i >= 0; --i) { 254 | h += v.charCodeAt(i); 255 | } 256 | if (h != hash) { 257 | pub.addMessage(messages, options.message, value); 258 | } 259 | }, 260 | 261 | compare: function (value, messages, options, $form) { 262 | if (options.skipOnEmpty && pub.isEmpty(value)) { 263 | return; 264 | } 265 | 266 | var compareValue, 267 | valid = true; 268 | if (options.compareAttribute === undefined) { 269 | compareValue = options.compareValue; 270 | } else { 271 | var attributes = $form.data('yiiActiveForm').attributes 272 | for (var i = attributes.length - 1; i >= 0; i--) { 273 | if (attributes[i].id === options.compareAttribute) { 274 | compareValue = $(attributes[i].input).val(); 275 | } 276 | } 277 | } 278 | 279 | if (options.type === 'number') { 280 | value = parseFloat(value); 281 | compareValue = parseFloat(compareValue); 282 | } 283 | switch (options.operator) { 284 | case '==': 285 | valid = value == compareValue; 286 | break; 287 | case '===': 288 | valid = value === compareValue; 289 | break; 290 | case '!=': 291 | valid = value != compareValue; 292 | break; 293 | case '!==': 294 | valid = value !== compareValue; 295 | break; 296 | case '>': 297 | valid = value > compareValue; 298 | break; 299 | case '>=': 300 | valid = value >= compareValue; 301 | break; 302 | case '<': 303 | valid = value < compareValue; 304 | break; 305 | case '<=': 306 | valid = value <= compareValue; 307 | break; 308 | default: 309 | valid = false; 310 | break; 311 | } 312 | 313 | if (!valid) { 314 | pub.addMessage(messages, options.message, value); 315 | } 316 | }, 317 | 318 | ip: function (value, messages, options) { 319 | if (options.skipOnEmpty && pub.isEmpty(value)) { 320 | return; 321 | } 322 | 323 | var negation = null, 324 | cidr = null, 325 | matches = new RegExp(options.ipParsePattern).exec(value); 326 | if (matches) { 327 | negation = matches[1] || null; 328 | value = matches[2]; 329 | cidr = matches[4] || null; 330 | } 331 | 332 | if (options.subnet === true && cidr === null) { 333 | pub.addMessage(messages, options.messages.noSubnet, value); 334 | return; 335 | } 336 | if (options.subnet === false && cidr !== null) { 337 | pub.addMessage(messages, options.messages.hasSubnet, value); 338 | return; 339 | } 340 | if (options.negation === false && negation !== null) { 341 | pub.addMessage(messages, options.messages.message, value); 342 | return; 343 | } 344 | 345 | var ipVersion = value.indexOf(':') === -1 ? 4 : 6; 346 | if (ipVersion == 6) { 347 | if (!(new RegExp(options.ipv6Pattern)).test(value)) { 348 | pub.addMessage(messages, options.messages.message, value); 349 | } 350 | if (!options.ipv6) { 351 | pub.addMessage(messages, options.messages.ipv6NotAllowed, value); 352 | } 353 | } else { 354 | if (!(new RegExp(options.ipv4Pattern)).test(value)) { 355 | pub.addMessage(messages, options.messages.message, value); 356 | } 357 | if (!options.ipv4) { 358 | pub.addMessage(messages, options.messages.ipv4NotAllowed, value); 359 | } 360 | } 361 | } 362 | }; 363 | 364 | function getUploadedFiles(attribute, messages, options) { 365 | // Skip validation if File API is not available 366 | if (typeof File === "undefined") { 367 | return []; 368 | } 369 | 370 | var files = $(attribute.input, attribute.$form).get(0).files; 371 | if (!files) { 372 | messages.push(options.message); 373 | return []; 374 | } 375 | 376 | if (files.length === 0) { 377 | if (!options.skipOnEmpty) { 378 | messages.push(options.uploadRequired); 379 | } 380 | 381 | return []; 382 | } 383 | 384 | if (options.maxFiles && options.maxFiles < files.length) { 385 | messages.push(options.tooMany); 386 | return []; 387 | } 388 | 389 | return files; 390 | } 391 | 392 | function validateFile(file, messages, options) { 393 | if (options.extensions && options.extensions.length > 0) { 394 | var index = file.name.lastIndexOf('.'); 395 | var ext = !~index ? '' : file.name.substr(index + 1, file.name.length).toLowerCase(); 396 | 397 | if (!~options.extensions.indexOf(ext)) { 398 | messages.push(options.wrongExtension.replace(/\{file\}/g, file.name)); 399 | } 400 | } 401 | 402 | if (options.mimeTypes && options.mimeTypes.length > 0) { 403 | if (!validateMimeType(options.mimeTypes, file.type)) { 404 | messages.push(options.wrongMimeType.replace(/\{file\}/g, file.name)); 405 | } 406 | } 407 | 408 | if (options.maxSize && options.maxSize < file.size) { 409 | messages.push(options.tooBig.replace(/\{file\}/g, file.name)); 410 | } 411 | 412 | if (options.minSize && options.minSize > file.size) { 413 | messages.push(options.tooSmall.replace(/\{file\}/g, file.name)); 414 | } 415 | } 416 | 417 | function validateMimeType(mimeTypes, fileType) { 418 | for (var i = 0, len = mimeTypes.length; i < len; i++) { 419 | if (new RegExp(mimeTypes[i]).test(fileType)) { 420 | return true; 421 | } 422 | } 423 | 424 | return false; 425 | } 426 | 427 | function validateImageSize(file, image, messages, options) { 428 | if (options.minWidth && image.width < options.minWidth) { 429 | messages.push(options.underWidth.replace(/\{file\}/g, file.name)); 430 | } 431 | 432 | if (options.maxWidth && image.width > options.maxWidth) { 433 | messages.push(options.overWidth.replace(/\{file\}/g, file.name)); 434 | } 435 | 436 | if (options.minHeight && image.height < options.minHeight) { 437 | messages.push(options.underHeight.replace(/\{file\}/g, file.name)); 438 | } 439 | 440 | if (options.maxHeight && image.height > options.maxHeight) { 441 | messages.push(options.overHeight.replace(/\{file\}/g, file.name)); 442 | } 443 | } 444 | 445 | return pub; 446 | })(jQuery); 447 | -------------------------------------------------------------------------------- /src/assets/yii.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Yii JavaScript module. 3 | */ 4 | 5 | /** 6 | * yii is the root module for all Yii JavaScript modules. 7 | * It implements a mechanism of organizing JavaScript code in modules through the function "yii.initModule()". 8 | * 9 | * Each module should be named as "x.y.z", where "x" stands for the root module (for the Yii core code, this is "yii"). 10 | * 11 | * A module may be structured as follows: 12 | * 13 | * ```javascript 14 | * window.yii.sample = (function($) { 15 | * var pub = { 16 | * // whether this module is currently active. If false, init() will not be called for this module 17 | * // it will also not be called for all its child modules. If this property is undefined, it means true. 18 | * isActive: true, 19 | * init: function() { 20 | * // ... module initialization code goes here ... 21 | * }, 22 | * 23 | * // ... other public functions and properties go here ... 24 | * }; 25 | * 26 | * // ... private functions and properties go here ... 27 | * 28 | * return pub; 29 | * })(window.jQuery); 30 | * ``` 31 | * 32 | * Using this structure, you can define public and private functions/properties for a module. 33 | * Private functions/properties are only visible within the module, while public functions/properties 34 | * may be accessed outside of the module. For example, you can access "yii.sample.isActive". 35 | * 36 | * You must call "yii.initModule()" once for the root module of all your modules. 37 | */ 38 | window.yii = (function ($) { 39 | var pub = { 40 | /** 41 | * List of JS or CSS URLs that can be loaded multiple times via AJAX requests. 42 | * Each item may be represented as either an absolute URL or a relative one. 43 | * Each item may contain a wildcard matching character `*`, that means one or more 44 | * any characters on the position. For example: 45 | * - `/css/*.css` will match any file ending with `.css` in the `css` directory of the current web site 46 | * - `http*://cdn.example.com/*` will match any files on domain `cdn.example.com`, loaded with HTTP or HTTPS 47 | * - `/js/myCustomScript.js?realm=*` will match file `/js/myCustomScript.js` with defined `realm` parameter 48 | */ 49 | reloadableScripts: [], 50 | /** 51 | * The selector for clickable elements that need to support confirmation and form submission. 52 | */ 53 | clickableSelector: 'a, button, input[type="submit"], input[type="button"], input[type="reset"], ' + 54 | 'input[type="image"]', 55 | /** 56 | * The selector for changeable elements that need to support confirmation and form submission. 57 | */ 58 | changeableSelector: 'select, input, textarea', 59 | 60 | /** 61 | * @return string|undefined the CSRF parameter name. Undefined is returned if CSRF validation is not enabled. 62 | */ 63 | getCsrfParam: function () { 64 | return $('meta[name=csrf-param]').attr('content'); 65 | }, 66 | 67 | /** 68 | * @return string|undefined the CSRF token. Undefined is returned if CSRF validation is not enabled. 69 | */ 70 | getCsrfToken: function () { 71 | return $('meta[name=csrf-token]').attr('content'); 72 | }, 73 | 74 | /** 75 | * Sets the CSRF token in the meta elements. 76 | * This method is provided so that you can update the CSRF token with the latest one you obtain from the server. 77 | * @param name the CSRF token name 78 | * @param value the CSRF token value 79 | */ 80 | setCsrfToken: function (name, value) { 81 | $('meta[name=csrf-param]').attr('content', name); 82 | $('meta[name=csrf-token]').attr('content', value); 83 | }, 84 | 85 | /** 86 | * Updates all form CSRF input fields with the latest CSRF token. 87 | * This method is provided to avoid cached forms containing outdated CSRF tokens. 88 | */ 89 | refreshCsrfToken: function () { 90 | var token = pub.getCsrfToken(); 91 | if (token) { 92 | $('form input[name="' + pub.getCsrfParam() + '"]').val(token); 93 | } 94 | }, 95 | 96 | /** 97 | * Displays a confirmation dialog. 98 | * The default implementation simply displays a js confirmation dialog. 99 | * You may override this by setting `yii.confirm`. 100 | * @param message the confirmation message. 101 | * @param ok a callback to be called when the user confirms the message 102 | * @param cancel a callback to be called when the user cancels the confirmation 103 | */ 104 | confirm: function (message, ok, cancel) { 105 | if (window.confirm(message)) { 106 | !ok || ok(); 107 | } else { 108 | !cancel || cancel(); 109 | } 110 | }, 111 | 112 | /** 113 | * Handles the action triggered by user. 114 | * This method recognizes the `data-method` attribute of the element. If the attribute exists, 115 | * the method will submit the form containing this element. If there is no containing form, a form 116 | * will be created and submitted using the method given by this attribute value (e.g. "post", "put"). 117 | * For hyperlinks, the form action will take the value of the "href" attribute of the link. 118 | * For other elements, either the containing form action or the current page URL will be used 119 | * as the form action URL. 120 | * 121 | * If the `data-method` attribute is not defined, the `href` attribute (if any) of the element 122 | * will be assigned to `window.location`. 123 | * 124 | * Starting from version 2.0.3, the `data-params` attribute is also recognized when you specify 125 | * `data-method`. The value of `data-params` should be a JSON representation of the data (name-value pairs) 126 | * that should be submitted as hidden inputs. For example, you may use the following code to generate 127 | * such a link: 128 | * 129 | * ```php 130 | * use yii\helpers\Html; 131 | * use yii\helpers\Json; 132 | * 133 | * echo Html::a('submit', ['site/foobar'], [ 134 | * 'data' => [ 135 | * 'method' => 'post', 136 | * 'params' => [ 137 | * 'name1' => 'value1', 138 | * 'name2' => 'value2', 139 | * ], 140 | * ], 141 | * ]); 142 | * ``` 143 | * 144 | * @param $e the jQuery representation of the element 145 | * @param event Related event 146 | */ 147 | handleAction: function ($e, event) { 148 | var $form = $e.attr('data-form') ? $('#' + $e.attr('data-form')) : $e.closest('form'), 149 | method = !$e.data('method') && $form ? $form.attr('method') : $e.data('method'), 150 | action = $e.attr('href'), 151 | isValidAction = action && action !== '#', 152 | params = $e.data('params'), 153 | areValidParams = params && $.isPlainObject(params); 154 | 155 | if (method === undefined) { 156 | if (isValidAction) { 157 | window.location.assign(action); 158 | } else if ($e.is(':submit') && $form.length) { 159 | $form.trigger('submit'); 160 | } 161 | return; 162 | } 163 | 164 | var oldMethod, 165 | oldAction, 166 | newForm = !$form.length; 167 | if (!newForm) { 168 | oldMethod = $form.attr('method'); 169 | $form.attr('method', method); 170 | if (isValidAction) { 171 | oldAction = $form.attr('action'); 172 | $form.attr('action', action); 173 | } 174 | } else { 175 | if (!isValidAction) { 176 | action = pub.getCurrentUrl(); 177 | } 178 | $form = $('', {method: method, action: action}); 179 | var target = $e.attr('target'); 180 | if (target) { 181 | $form.attr('target', target); 182 | } 183 | if (!/(get|post)/i.test(method)) { 184 | $form.append($('', {name: '_method', value: method, type: 'hidden'})); 185 | method = 'post'; 186 | $form.attr('method', method); 187 | } 188 | if (/post/i.test(method)) { 189 | var csrfParam = pub.getCsrfParam(); 190 | if (csrfParam) { 191 | $form.append($('', {name: csrfParam, value: pub.getCsrfToken(), type: 'hidden'})); 192 | } 193 | } 194 | $form.hide().appendTo('body'); 195 | } 196 | 197 | var activeFormData = $form.data('yiiActiveForm'); 198 | if (activeFormData) { 199 | // Remember the element triggered the form submission. This is used by yii.activeForm.js. 200 | activeFormData.submitObject = $e; 201 | } 202 | 203 | if (areValidParams) { 204 | $.each(params, function (name, value) { 205 | $form.append($('').attr({name: name, value: value, type: 'hidden'})); 206 | }); 207 | } 208 | 209 | $form.trigger('submit'); 210 | 211 | $.when($form.data('yiiSubmitFinalizePromise')).done(function () { 212 | if (newForm) { 213 | $form.remove(); 214 | return; 215 | } 216 | 217 | if (oldAction !== undefined) { 218 | $form.attr('action', oldAction); 219 | } 220 | $form.attr('method', oldMethod); 221 | 222 | if (areValidParams) { 223 | $.each(params, function (name) { 224 | $('input[name="' + name + '"]', $form).remove(); 225 | }); 226 | } 227 | }); 228 | }, 229 | 230 | getQueryParams: function (url) { 231 | var pos = url.indexOf('?'); 232 | if (pos < 0) { 233 | return {}; 234 | } 235 | 236 | var pairs = $.grep(url.substring(pos + 1).split('#')[0].split('&'), function (value) { 237 | return value !== ''; 238 | }); 239 | var params = {}; 240 | 241 | for (var i = 0, len = pairs.length; i < len; i++) { 242 | var pair = pairs[i].split('='); 243 | var name = decodeURIComponent(pair[0].replace(/\+/g, '%20')); 244 | var value = decodeURIComponent(pair[1].replace(/\+/g, '%20')); 245 | if (!name.length) { 246 | continue; 247 | } 248 | if (params[name] === undefined) { 249 | params[name] = value || ''; 250 | } else { 251 | if (!$.isArray(params[name])) { 252 | params[name] = [params[name]]; 253 | } 254 | params[name].push(value || ''); 255 | } 256 | } 257 | 258 | return params; 259 | }, 260 | 261 | initModule: function (module) { 262 | if (module.isActive !== undefined && !module.isActive) { 263 | return; 264 | } 265 | if ($.isFunction(module.init)) { 266 | module.init(); 267 | } 268 | $.each(module, function () { 269 | if ($.isPlainObject(this)) { 270 | pub.initModule(this); 271 | } 272 | }); 273 | }, 274 | 275 | init: function () { 276 | initCsrfHandler(); 277 | initRedirectHandler(); 278 | initAssetFilters(); 279 | initDataMethods(); 280 | }, 281 | 282 | /** 283 | * Returns the URL of the current page without params and trailing slash. Separated and made public for testing. 284 | * @returns {string} 285 | */ 286 | getBaseCurrentUrl: function () { 287 | return window.location.protocol + '//' + window.location.host; 288 | }, 289 | 290 | /** 291 | * Returns the URL of the current page. Used for testing, you can always call `window.location.href` manually 292 | * instead. 293 | * @returns {string} 294 | */ 295 | getCurrentUrl: function () { 296 | return window.location.href; 297 | } 298 | }; 299 | 300 | function initCsrfHandler() { 301 | // automatically send CSRF token for all AJAX requests 302 | $.ajaxPrefilter(function (options, originalOptions, xhr) { 303 | if (!options.crossDomain && pub.getCsrfParam()) { 304 | xhr.setRequestHeader('X-CSRF-Token', pub.getCsrfToken()); 305 | } 306 | }); 307 | pub.refreshCsrfToken(); 308 | } 309 | 310 | function initRedirectHandler() { 311 | // handle AJAX redirection 312 | $(document).ajaxComplete(function (event, xhr) { 313 | var url = xhr && xhr.getResponseHeader('X-Redirect'); 314 | if (url) { 315 | window.location.assign(url); 316 | } 317 | }); 318 | } 319 | 320 | function initAssetFilters() { 321 | /** 322 | * Used for storing loaded scripts and information about loading each script if it's in the process of loading. 323 | * A single script can have one of the following values: 324 | * 325 | * - `undefined` - script was not loaded at all before or was loaded with error last time. 326 | * - `true` (boolean) - script was successfully loaded. 327 | * - object - script is currently loading. 328 | * 329 | * In case of a value being an object the properties are: 330 | * - `xhrList` - represents a queue of XHR requests sent to the same URL (related with this script) in the same 331 | * small period of time. 332 | * - `xhrDone` - boolean, acts like a locking mechanism. When one of the XHR requests in the queue is 333 | * successfully completed, it will abort the rest of concurrent requests to the same URL until cleanup is done 334 | * to prevent possible errors and race conditions. 335 | * @type {{}} 336 | */ 337 | var loadedScripts = {}; 338 | 339 | $('script[src]').each(function () { 340 | var url = getAbsoluteUrl(this.src); 341 | loadedScripts[url] = true; 342 | }); 343 | 344 | $.ajaxPrefilter('script', function (options, originalOptions, xhr) { 345 | if (options.dataType == 'jsonp') { 346 | return; 347 | } 348 | 349 | var url = getAbsoluteUrl(options.url), 350 | forbiddenRepeatedLoad = loadedScripts[url] === true && !isReloadableAsset(url), 351 | cleanupRunning = loadedScripts[url] !== undefined && loadedScripts[url]['xhrDone'] === true; 352 | 353 | if (forbiddenRepeatedLoad || cleanupRunning) { 354 | xhr.abort(); 355 | return; 356 | } 357 | 358 | if (loadedScripts[url] === undefined || loadedScripts[url] === true) { 359 | loadedScripts[url] = { 360 | xhrList: [], 361 | xhrDone: false 362 | }; 363 | } 364 | 365 | xhr.done(function (data, textStatus, jqXHR) { 366 | // If multiple requests were successfully loaded, perform cleanup only once 367 | if (loadedScripts[jqXHR.yiiUrl]['xhrDone'] === true) { 368 | return; 369 | } 370 | 371 | loadedScripts[jqXHR.yiiUrl]['xhrDone'] = true; 372 | 373 | for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { 374 | var singleXhr = loadedScripts[jqXHR.yiiUrl]['xhrList'][i]; 375 | if (singleXhr && singleXhr.readyState !== XMLHttpRequest.DONE) { 376 | singleXhr.abort(); 377 | } 378 | } 379 | 380 | loadedScripts[jqXHR.yiiUrl] = true; 381 | }).fail(function (jqXHR, textStatus) { 382 | if (textStatus === 'abort') { 383 | return; 384 | } 385 | 386 | delete loadedScripts[jqXHR.yiiUrl]['xhrList'][jqXHR.yiiIndex]; 387 | 388 | var allFailed = true; 389 | for (var i = 0, len = loadedScripts[jqXHR.yiiUrl]['xhrList'].length; i < len; i++) { 390 | if (loadedScripts[jqXHR.yiiUrl]['xhrList'][i]) { 391 | allFailed = false; 392 | } 393 | } 394 | 395 | if (allFailed) { 396 | delete loadedScripts[jqXHR.yiiUrl]; 397 | } 398 | }); 399 | // Use prefix for custom XHR properties to avoid possible conflicts with existing properties 400 | xhr.yiiIndex = loadedScripts[url]['xhrList'].length; 401 | xhr.yiiUrl = url; 402 | 403 | loadedScripts[url]['xhrList'][xhr.yiiIndex] = xhr; 404 | }); 405 | 406 | $(document).ajaxComplete(function () { 407 | var styleSheets = []; 408 | $('link[rel=stylesheet]').each(function () { 409 | var url = getAbsoluteUrl(this.href); 410 | if (isReloadableAsset(url)) { 411 | return; 412 | } 413 | 414 | $.inArray(url, styleSheets) === -1 ? styleSheets.push(url) : $(this).remove(); 415 | }); 416 | }); 417 | } 418 | 419 | function initDataMethods() { 420 | var handler = function (event) { 421 | var $this = $(this), 422 | method = $this.data('method'), 423 | message = $this.data('confirm'), 424 | form = $this.data('form'); 425 | 426 | if (method === undefined && message === undefined && form === undefined) { 427 | return true; 428 | } 429 | 430 | if (message !== undefined) { 431 | $.proxy(pub.confirm, this)(message, function () { 432 | pub.handleAction($this, event); 433 | }); 434 | } else { 435 | pub.handleAction($this, event); 436 | } 437 | event.stopImmediatePropagation(); 438 | return false; 439 | }; 440 | 441 | // handle data-confirm and data-method for clickable and changeable elements 442 | $(document).on('click.yii', pub.clickableSelector, handler) 443 | .on('change.yii', pub.changeableSelector, handler); 444 | } 445 | 446 | function isReloadableAsset(url) { 447 | for (var i = 0; i < pub.reloadableScripts.length; i++) { 448 | var rule = getAbsoluteUrl(pub.reloadableScripts[i]); 449 | var match = new RegExp("^" + escapeRegExp(rule).split('\\*').join('.+') + "$").test(url); 450 | if (match === true) { 451 | return true; 452 | } 453 | } 454 | 455 | return false; 456 | } 457 | 458 | // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex 459 | function escapeRegExp(str) { 460 | return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); 461 | } 462 | 463 | /** 464 | * Returns absolute URL based on the given URL 465 | * @param {string} url Initial URL 466 | * @returns {string} 467 | */ 468 | function getAbsoluteUrl(url) { 469 | return url.charAt(0) === '/' ? pub.getBaseCurrentUrl() + url : url; 470 | } 471 | 472 | return pub; 473 | })(window.jQuery); 474 | 475 | window.jQuery(function () { 476 | window.yii.initModule(window.yii); 477 | }); 478 | -------------------------------------------------------------------------------- /src/assets/yii.activeForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Yii form widget. 3 | * 4 | * This is the JavaScript widget used by the yii\widgets\ActiveForm widget. 5 | */ 6 | (function ($) { 7 | 8 | $.fn.yiiActiveForm = function (method) { 9 | if (methods[method]) { 10 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 11 | } else if (typeof method === 'object' || !method) { 12 | return methods.init.apply(this, arguments); 13 | } else { 14 | $.error('Method ' + method + ' does not exist on jQuery.yiiActiveForm'); 15 | return false; 16 | } 17 | }; 18 | 19 | var events = { 20 | /** 21 | * beforeValidate event is triggered before validating the whole form. 22 | * The signature of the event handler should be: 23 | * function (event, messages, deferreds) 24 | * where 25 | * - event: an Event object. 26 | * - messages: an associative array with keys being attribute IDs and values being error message arrays 27 | * for the corresponding attributes. 28 | * - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation. 29 | * 30 | * If the handler returns a boolean false, it will stop further form validation after this event. And as 31 | * a result, afterValidate event will not be triggered. 32 | */ 33 | beforeValidate: 'beforeValidate', 34 | /** 35 | * afterValidate event is triggered after validating the whole form. 36 | * The signature of the event handler should be: 37 | * function (event, messages, errorAttributes) 38 | * where 39 | * - event: an Event object. 40 | * - messages: an associative array with keys being attribute IDs and values being error message arrays 41 | * for the corresponding attributes. 42 | * - errorAttributes: an array of attributes that have validation errors. Please refer to attributeDefaults for the structure of this parameter. 43 | */ 44 | afterValidate: 'afterValidate', 45 | /** 46 | * beforeValidateAttribute event is triggered before validating an attribute. 47 | * The signature of the event handler should be: 48 | * function (event, attribute, messages, deferreds) 49 | * where 50 | * - event: an Event object. 51 | * - attribute: the attribute to be validated. Please refer to attributeDefaults for the structure of this parameter. 52 | * - messages: an array to which you can add validation error messages for the specified attribute. 53 | * - deferreds: an array of Deferred objects. You can use deferreds.add(callback) to add a new deferred validation. 54 | * 55 | * If the handler returns a boolean false, it will stop further validation of the specified attribute. 56 | * And as a result, afterValidateAttribute event will not be triggered. 57 | */ 58 | beforeValidateAttribute: 'beforeValidateAttribute', 59 | /** 60 | * afterValidateAttribute event is triggered after validating the whole form and each attribute. 61 | * The signature of the event handler should be: 62 | * function (event, attribute, messages) 63 | * where 64 | * - event: an Event object. 65 | * - attribute: the attribute being validated. Please refer to attributeDefaults for the structure of this parameter. 66 | * - messages: an array to which you can add additional validation error messages for the specified attribute. 67 | */ 68 | afterValidateAttribute: 'afterValidateAttribute', 69 | /** 70 | * beforeSubmit event is triggered before submitting the form after all validations have passed. 71 | * The signature of the event handler should be: 72 | * function (event) 73 | * where event is an Event object. 74 | * 75 | * If the handler returns a boolean false, it will stop form submission. 76 | */ 77 | beforeSubmit: 'beforeSubmit', 78 | /** 79 | * ajaxBeforeSend event is triggered before sending an AJAX request for AJAX-based validation. 80 | * The signature of the event handler should be: 81 | * function (event, jqXHR, settings) 82 | * where 83 | * - event: an Event object. 84 | * - jqXHR: a jqXHR object 85 | * - settings: the settings for the AJAX request 86 | */ 87 | ajaxBeforeSend: 'ajaxBeforeSend', 88 | /** 89 | * ajaxComplete event is triggered after completing an AJAX request for AJAX-based validation. 90 | * The signature of the event handler should be: 91 | * function (event, jqXHR, textStatus) 92 | * where 93 | * - event: an Event object. 94 | * - jqXHR: a jqXHR object 95 | * - textStatus: the status of the request ("success", "notmodified", "error", "timeout", "abort", or "parsererror"). 96 | */ 97 | ajaxComplete: 'ajaxComplete', 98 | /** 99 | * afterInit event is triggered after yii activeForm init. 100 | * The signature of the event handler should be: 101 | * function (event) 102 | * where 103 | * - event: an Event object. 104 | */ 105 | afterInit: 'afterInit' 106 | }; 107 | 108 | // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveForm::getClientOptions() as well 109 | var defaults = { 110 | // whether to encode the error summary 111 | encodeErrorSummary: true, 112 | // the jQuery selector for the error summary 113 | errorSummary: '.error-summary', 114 | // whether to perform validation before submitting the form. 115 | validateOnSubmit: true, 116 | // the container CSS class representing the corresponding attribute has validation error 117 | errorCssClass: 'has-error', 118 | // the container CSS class representing the corresponding attribute passes validation 119 | successCssClass: 'has-success', 120 | // the container CSS class representing the corresponding attribute is being validated 121 | validatingCssClass: 'validating', 122 | // the GET parameter name indicating an AJAX-based validation 123 | ajaxParam: 'ajax', 124 | // the type of data that you're expecting back from the server 125 | ajaxDataType: 'json', 126 | // the URL for performing AJAX-based validation. If not set, it will use the the form's action 127 | validationUrl: undefined, 128 | // whether to scroll to first visible error after validation. 129 | scrollToError: true, 130 | // offset in pixels that should be added when scrolling to the first error. 131 | scrollToErrorOffset: 0, 132 | // where to add validation class: container or input 133 | validationStateOn: 'container' 134 | }; 135 | 136 | // NOTE: If you change any of these defaults, make sure you update yii\widgets\ActiveField::getClientOptions() as well 137 | var attributeDefaults = { 138 | // a unique ID identifying an attribute (e.g. "loginform-username") in a form 139 | id: undefined, 140 | // attribute name or expression (e.g. "[0]content" for tabular input) 141 | name: undefined, 142 | // the jQuery selector of the container of the input field 143 | container: undefined, 144 | // the jQuery selector of the input field under the context of the form 145 | input: undefined, 146 | // the jQuery selector of the error tag under the context of the container 147 | error: '.help-block', 148 | // whether to encode the error 149 | encodeError: true, 150 | // whether to perform validation when a change is detected on the input 151 | validateOnChange: true, 152 | // whether to perform validation when the input loses focus 153 | validateOnBlur: true, 154 | // whether to perform validation when the user is typing. 155 | validateOnType: false, 156 | // number of milliseconds that the validation should be delayed when a user is typing in the input field. 157 | validationDelay: 500, 158 | // whether to enable AJAX-based validation. 159 | enableAjaxValidation: false, 160 | // function (attribute, value, messages, deferred, $form), the client-side validation function. 161 | validate: undefined, 162 | // status of the input field, 0: empty, not entered before, 1: validated, 2: pending validation, 3: validating 163 | status: 0, 164 | // whether the validation is cancelled by beforeValidateAttribute event handler 165 | cancelled: false, 166 | // the value of the input 167 | value: undefined, 168 | // whether to update aria-invalid attribute after validation 169 | updateAriaInvalid: true 170 | }; 171 | 172 | 173 | var submitDefer; 174 | 175 | var setSubmitFinalizeDefer = function($form) { 176 | submitDefer = $.Deferred(); 177 | $form.data('yiiSubmitFinalizePromise', submitDefer.promise()); 178 | }; 179 | 180 | // finalize yii.js $form.submit 181 | var submitFinalize = function($form) { 182 | if(submitDefer) { 183 | submitDefer.resolve(); 184 | submitDefer = undefined; 185 | $form.removeData('yiiSubmitFinalizePromise'); 186 | } 187 | }; 188 | 189 | 190 | var methods = { 191 | init: function (attributes, options) { 192 | return this.each(function () { 193 | var $form = $(this); 194 | if ($form.data('yiiActiveForm')) { 195 | return; 196 | } 197 | 198 | var settings = $.extend({}, defaults, options || {}); 199 | if (settings.validationUrl === undefined) { 200 | settings.validationUrl = $form.attr('action'); 201 | } 202 | 203 | $.each(attributes, function (i) { 204 | attributes[i] = $.extend({value: getValue($form, this)}, attributeDefaults, this); 205 | watchAttribute($form, attributes[i]); 206 | }); 207 | 208 | $form.data('yiiActiveForm', { 209 | settings: settings, 210 | attributes: attributes, 211 | submitting: false, 212 | validated: false, 213 | options: getFormOptions($form) 214 | }); 215 | 216 | /** 217 | * Clean up error status when the form is reset. 218 | * Note that $form.on('reset', ...) does work because the "reset" event does not bubble on IE. 219 | */ 220 | $form.on('reset.yiiActiveForm', methods.resetForm); 221 | 222 | if (settings.validateOnSubmit) { 223 | $form.on('mouseup.yiiActiveForm keyup.yiiActiveForm', ':submit', function () { 224 | $form.data('yiiActiveForm').submitObject = $(this); 225 | }); 226 | $form.on('submit.yiiActiveForm', methods.submitForm); 227 | } 228 | var event = $.Event(events.afterInit); 229 | $form.trigger(event); 230 | }); 231 | }, 232 | 233 | // add a new attribute to the form dynamically. 234 | // please refer to attributeDefaults for the structure of attribute 235 | add: function (attribute) { 236 | var $form = $(this); 237 | attribute = $.extend({value: getValue($form, attribute)}, attributeDefaults, attribute); 238 | $form.data('yiiActiveForm').attributes.push(attribute); 239 | watchAttribute($form, attribute); 240 | }, 241 | 242 | // remove the attribute with the specified ID from the form 243 | remove: function (id) { 244 | var $form = $(this), 245 | attributes = $form.data('yiiActiveForm').attributes, 246 | index = -1, 247 | attribute = undefined; 248 | $.each(attributes, function (i) { 249 | if (attributes[i]['id'] == id) { 250 | index = i; 251 | attribute = attributes[i]; 252 | return false; 253 | } 254 | }); 255 | if (index >= 0) { 256 | attributes.splice(index, 1); 257 | unwatchAttribute($form, attribute); 258 | } 259 | 260 | return attribute; 261 | }, 262 | 263 | // manually trigger the validation of the attribute with the specified ID 264 | validateAttribute: function (id) { 265 | var attribute = methods.find.call(this, id); 266 | if (attribute != undefined) { 267 | validateAttribute($(this), attribute, true); 268 | } 269 | }, 270 | 271 | // find an attribute config based on the specified attribute ID 272 | find: function (id) { 273 | var attributes = $(this).data('yiiActiveForm').attributes, 274 | result = undefined; 275 | $.each(attributes, function (i) { 276 | if (attributes[i]['id'] == id) { 277 | result = attributes[i]; 278 | return false; 279 | } 280 | }); 281 | return result; 282 | }, 283 | 284 | destroy: function () { 285 | return this.each(function () { 286 | $(this).off('.yiiActiveForm'); 287 | $(this).removeData('yiiActiveForm'); 288 | }); 289 | }, 290 | 291 | data: function () { 292 | return this.data('yiiActiveForm'); 293 | }, 294 | 295 | // validate all applicable inputs in the form 296 | validate: function (forceValidate) { 297 | if (forceValidate) { 298 | $(this).data('yiiActiveForm').submitting = true; 299 | } 300 | 301 | var $form = $(this), 302 | data = $form.data('yiiActiveForm'), 303 | needAjaxValidation = false, 304 | messages = {}, 305 | deferreds = deferredArray(), 306 | submitting = data.submitting; 307 | 308 | if (submitting) { 309 | var event = $.Event(events.beforeValidate); 310 | $form.trigger(event, [messages, deferreds]); 311 | 312 | if (event.result === false) { 313 | data.submitting = false; 314 | submitFinalize($form); 315 | return; 316 | } 317 | } 318 | 319 | // client-side validation 320 | $.each(data.attributes, function () { 321 | this.$form = $form; 322 | if (!$(this.input).is(":disabled")) { 323 | this.cancelled = false; 324 | // perform validation only if the form is being submitted or if an attribute is pending validation 325 | if (data.submitting || this.status === 2 || this.status === 3) { 326 | var msg = messages[this.id]; 327 | if (msg === undefined) { 328 | msg = []; 329 | messages[this.id] = msg; 330 | } 331 | var event = $.Event(events.beforeValidateAttribute); 332 | $form.trigger(event, [this, msg, deferreds]); 333 | if (event.result !== false) { 334 | if (this.validate) { 335 | this.validate(this, getValue($form, this), msg, deferreds, $form); 336 | } 337 | if (this.enableAjaxValidation) { 338 | needAjaxValidation = true; 339 | } 340 | } else { 341 | this.cancelled = true; 342 | } 343 | } 344 | } 345 | }); 346 | 347 | // ajax validation 348 | $.when.apply(this, deferreds).always(function() { 349 | // Remove empty message arrays 350 | for (var i in messages) { 351 | if (0 === messages[i].length) { 352 | delete messages[i]; 353 | } 354 | } 355 | if (needAjaxValidation && ($.isEmptyObject(messages) || data.submitting)) { 356 | var $button = data.submitObject, 357 | extData = '&' + data.settings.ajaxParam + '=' + $form.attr('id'); 358 | if ($button && $button.length && $button.attr('name')) { 359 | extData += '&' + $button.attr('name') + '=' + $button.attr('value'); 360 | } 361 | $.ajax({ 362 | url: data.settings.validationUrl, 363 | type: $form.attr('method'), 364 | data: $form.serialize() + extData, 365 | dataType: data.settings.ajaxDataType, 366 | complete: function (jqXHR, textStatus) { 367 | $form.trigger(events.ajaxComplete, [jqXHR, textStatus]); 368 | }, 369 | beforeSend: function (jqXHR, settings) { 370 | $form.trigger(events.ajaxBeforeSend, [jqXHR, settings]); 371 | }, 372 | success: function (msgs) { 373 | if (msgs !== null && typeof msgs === 'object') { 374 | $.each(data.attributes, function () { 375 | if (!this.enableAjaxValidation || this.cancelled) { 376 | delete msgs[this.id]; 377 | } 378 | }); 379 | updateInputs($form, $.extend(messages, msgs), submitting); 380 | } else { 381 | updateInputs($form, messages, submitting); 382 | } 383 | }, 384 | error: function () { 385 | data.submitting = false; 386 | submitFinalize($form); 387 | } 388 | }); 389 | } else if (data.submitting) { 390 | // delay callback so that the form can be submitted without problem 391 | window.setTimeout(function () { 392 | updateInputs($form, messages, submitting); 393 | }, 200); 394 | } else { 395 | updateInputs($form, messages, submitting); 396 | } 397 | }); 398 | }, 399 | 400 | submitForm: function () { 401 | var $form = $(this), 402 | data = $form.data('yiiActiveForm'); 403 | 404 | if (data.validated) { 405 | // Second submit's call (from validate/updateInputs) 406 | data.submitting = false; 407 | var event = $.Event(events.beforeSubmit); 408 | $form.trigger(event); 409 | if (event.result === false) { 410 | data.validated = false; 411 | submitFinalize($form); 412 | return false; 413 | } 414 | updateHiddenButton($form); 415 | return true; // continue submitting the form since validation passes 416 | } else { 417 | // First submit's call (from yii.js/handleAction) - execute validating 418 | setSubmitFinalizeDefer($form); 419 | 420 | if (data.settings.timer !== undefined) { 421 | clearTimeout(data.settings.timer); 422 | } 423 | data.submitting = true; 424 | methods.validate.call($form); 425 | return false; 426 | } 427 | }, 428 | 429 | resetForm: function () { 430 | var $form = $(this); 431 | var data = $form.data('yiiActiveForm'); 432 | // Because we bind directly to a form reset event instead of a reset button (that may not exist), 433 | // when this function is executed form input values have not been reset yet. 434 | // Therefore we do the actual reset work through setTimeout. 435 | window.setTimeout(function () { 436 | $.each(data.attributes, function () { 437 | // Without setTimeout() we would get the input values that are not reset yet. 438 | this.value = getValue($form, this); 439 | this.status = 0; 440 | var $container = $form.find(this.container), 441 | $input = findInput($form, this), 442 | $errorElement = data.settings.validationStateOn === 'input' ? $input : $container; 443 | 444 | $errorElement.removeClass( 445 | data.settings.validatingCssClass + ' ' + 446 | data.settings.errorCssClass + ' ' + 447 | data.settings.successCssClass 448 | ); 449 | $container.find(this.error).html(''); 450 | }); 451 | $form.find(data.settings.errorSummary).hide().find('ul').html(''); 452 | }, 1); 453 | }, 454 | 455 | /** 456 | * Updates error messages, input containers, and optionally summary as well. 457 | * If an attribute is missing from messages, it is considered valid. 458 | * @param messages array the validation error messages, indexed by attribute IDs 459 | * @param summary whether to update summary as well. 460 | */ 461 | updateMessages: function (messages, summary) { 462 | var $form = $(this); 463 | var data = $form.data('yiiActiveForm'); 464 | $.each(data.attributes, function () { 465 | updateInput($form, this, messages); 466 | }); 467 | if (summary) { 468 | updateSummary($form, messages); 469 | } 470 | }, 471 | 472 | /** 473 | * Updates error messages and input container of a single attribute. 474 | * If messages is empty, the attribute is considered valid. 475 | * @param id attribute ID 476 | * @param messages array with error messages 477 | */ 478 | updateAttribute: function(id, messages) { 479 | var attribute = methods.find.call(this, id); 480 | if (attribute != undefined) { 481 | var msg = {}; 482 | msg[id] = messages; 483 | updateInput($(this), attribute, msg); 484 | } 485 | } 486 | 487 | }; 488 | 489 | var watchAttribute = function ($form, attribute) { 490 | var $input = findInput($form, attribute); 491 | if (attribute.validateOnChange) { 492 | $input.on('change.yiiActiveForm', function () { 493 | validateAttribute($form, attribute, false); 494 | }); 495 | } 496 | if (attribute.validateOnBlur) { 497 | $input.on('blur.yiiActiveForm', function () { 498 | if (attribute.status == 0 || attribute.status == 1) { 499 | validateAttribute($form, attribute, true); 500 | } 501 | }); 502 | } 503 | if (attribute.validateOnType) { 504 | $input.on('keyup.yiiActiveForm', function (e) { 505 | if ($.inArray(e.which, [16, 17, 18, 37, 38, 39, 40]) !== -1 ) { 506 | return; 507 | } 508 | if (attribute.value !== getValue($form, attribute)) { 509 | validateAttribute($form, attribute, false, attribute.validationDelay); 510 | } 511 | }); 512 | } 513 | }; 514 | 515 | var unwatchAttribute = function ($form, attribute) { 516 | findInput($form, attribute).off('.yiiActiveForm'); 517 | }; 518 | 519 | var validateAttribute = function ($form, attribute, forceValidate, validationDelay) { 520 | var data = $form.data('yiiActiveForm'); 521 | 522 | if (forceValidate) { 523 | attribute.status = 2; 524 | } 525 | $.each(data.attributes, function () { 526 | if (this.value !== getValue($form, this)) { 527 | this.status = 2; 528 | forceValidate = true; 529 | } 530 | }); 531 | if (!forceValidate) { 532 | return; 533 | } 534 | 535 | if (data.settings.timer !== undefined) { 536 | clearTimeout(data.settings.timer); 537 | } 538 | data.settings.timer = window.setTimeout(function () { 539 | if (data.submitting || $form.is(':hidden')) { 540 | return; 541 | } 542 | $.each(data.attributes, function () { 543 | if (this.status === 2) { 544 | this.status = 3; 545 | $form.find(this.container).addClass(data.settings.validatingCssClass); 546 | } 547 | }); 548 | methods.validate.call($form); 549 | }, validationDelay ? validationDelay : 200); 550 | }; 551 | 552 | /** 553 | * Returns an array prototype with a shortcut method for adding a new deferred. 554 | * The context of the callback will be the deferred object so it can be resolved like ```this.resolve()``` 555 | * @returns Array 556 | */ 557 | var deferredArray = function () { 558 | var array = []; 559 | array.add = function(callback) { 560 | this.push(new $.Deferred(callback)); 561 | }; 562 | return array; 563 | }; 564 | 565 | var buttonOptions = ['action', 'target', 'method', 'enctype']; 566 | 567 | /** 568 | * Returns current form options 569 | * @param $form 570 | * @returns object Object with button of form options 571 | */ 572 | var getFormOptions = function ($form) { 573 | var attributes = {}; 574 | for (var i = 0; i < buttonOptions.length; i++) { 575 | attributes[buttonOptions[i]] = $form.attr(buttonOptions[i]); 576 | } 577 | 578 | return attributes; 579 | }; 580 | 581 | /** 582 | * Applies temporary form options related to submit button 583 | * @param $form the form jQuery object 584 | * @param $button the button jQuery object 585 | */ 586 | var applyButtonOptions = function ($form, $button) { 587 | for (var i = 0; i < buttonOptions.length; i++) { 588 | var value = $button.attr('form' + buttonOptions[i]); 589 | if (value) { 590 | $form.attr(buttonOptions[i], value); 591 | } 592 | } 593 | }; 594 | 595 | /** 596 | * Restores original form options 597 | * @param $form the form jQuery object 598 | */ 599 | var restoreButtonOptions = function ($form) { 600 | var data = $form.data('yiiActiveForm'); 601 | 602 | for (var i = 0; i < buttonOptions.length; i++) { 603 | $form.attr(buttonOptions[i], data.options[buttonOptions[i]] || null); 604 | } 605 | }; 606 | 607 | /** 608 | * Updates the error messages and the input containers for all applicable attributes 609 | * @param $form the form jQuery object 610 | * @param messages array the validation error messages 611 | * @param submitting whether this method is called after validation triggered by form submission 612 | */ 613 | var updateInputs = function ($form, messages, submitting) { 614 | var data = $form.data('yiiActiveForm'); 615 | 616 | if (data === undefined) { 617 | return false; 618 | } 619 | 620 | if (submitting) { 621 | var errorAttributes = []; 622 | $.each(data.attributes, function () { 623 | if (!$(this.input).is(":disabled") && !this.cancelled && updateInput($form, this, messages)) { 624 | errorAttributes.push(this); 625 | } 626 | }); 627 | 628 | $form.trigger(events.afterValidate, [messages, errorAttributes]); 629 | 630 | updateSummary($form, messages); 631 | 632 | if (errorAttributes.length) { 633 | if (data.settings.scrollToError) { 634 | var top = $form.find($.map(errorAttributes, function(attribute) { 635 | return attribute.input; 636 | }).join(',')).first().closest(':visible').offset().top - data.settings.scrollToErrorOffset; 637 | if (top < 0) { 638 | top = 0; 639 | } else if (top > $(document).height()) { 640 | top = $(document).height(); 641 | } 642 | var wtop = $(window).scrollTop(); 643 | if (top < wtop || top > wtop + $(window).height()) { 644 | $(window).scrollTop(top); 645 | } 646 | } 647 | data.submitting = false; 648 | } else { 649 | data.validated = true; 650 | if (data.submitObject) { 651 | applyButtonOptions($form, data.submitObject); 652 | } 653 | $form.submit(); 654 | if (data.submitObject) { 655 | restoreButtonOptions($form); 656 | } 657 | } 658 | } else { 659 | $.each(data.attributes, function () { 660 | if (!this.cancelled && (this.status === 2 || this.status === 3)) { 661 | updateInput($form, this, messages); 662 | } 663 | }); 664 | } 665 | submitFinalize($form); 666 | }; 667 | 668 | /** 669 | * Updates hidden field that represents clicked submit button. 670 | * @param $form the form jQuery object. 671 | */ 672 | var updateHiddenButton = function ($form) { 673 | var data = $form.data('yiiActiveForm'); 674 | var $button = data.submitObject || $form.find(':submit:first'); 675 | // TODO: if the submission is caused by "change" event, it will not work 676 | if ($button.length && $button.attr('type') == 'submit' && $button.attr('name')) { 677 | // simulate button input value 678 | var $hiddenButton = $('input[type="hidden"][name="' + $button.attr('name') + '"]', $form); 679 | if (!$hiddenButton.length) { 680 | $('').attr({ 681 | type: 'hidden', 682 | name: $button.attr('name'), 683 | value: $button.attr('value') 684 | }).appendTo($form); 685 | } else { 686 | $hiddenButton.attr('value', $button.attr('value')); 687 | } 688 | } 689 | }; 690 | 691 | /** 692 | * Updates the error message and the input container for a particular attribute. 693 | * @param $form the form jQuery object 694 | * @param attribute object the configuration for a particular attribute. 695 | * @param messages array the validation error messages 696 | * @return boolean whether there is a validation error for the specified attribute 697 | */ 698 | var updateInput = function ($form, attribute, messages) { 699 | var data = $form.data('yiiActiveForm'), 700 | $input = findInput($form, attribute), 701 | hasError = false; 702 | 703 | if (!$.isArray(messages[attribute.id])) { 704 | messages[attribute.id] = []; 705 | } 706 | 707 | attribute.status = 1; 708 | if ($input.length) { 709 | hasError = messages[attribute.id].length > 0; 710 | var $container = $form.find(attribute.container); 711 | var $error = $container.find(attribute.error); 712 | updateAriaInvalid($form, attribute, hasError); 713 | 714 | var $errorElement = data.settings.validationStateOn === 'input' ? $input : $container; 715 | 716 | if (hasError) { 717 | if (attribute.encodeError) { 718 | $error.text(messages[attribute.id][0]); 719 | } else { 720 | $error.html(messages[attribute.id][0]); 721 | } 722 | $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.successCssClass) 723 | .addClass(data.settings.errorCssClass); 724 | } else { 725 | $error.empty(); 726 | $errorElement.removeClass(data.settings.validatingCssClass + ' ' + data.settings.errorCssClass + ' ') 727 | .addClass(data.settings.successCssClass); 728 | } 729 | attribute.value = getValue($form, attribute); 730 | } 731 | 732 | $form.trigger(events.afterValidateAttribute, [attribute, messages[attribute.id]]); 733 | 734 | return hasError; 735 | }; 736 | 737 | /** 738 | * Updates the error summary. 739 | * @param $form the form jQuery object 740 | * @param messages array the validation error messages 741 | */ 742 | var updateSummary = function ($form, messages) { 743 | var data = $form.data('yiiActiveForm'), 744 | $summary = $form.find(data.settings.errorSummary), 745 | $ul = $summary.find('ul').empty(); 746 | 747 | if ($summary.length && messages) { 748 | $.each(data.attributes, function () { 749 | if ($.isArray(messages[this.id]) && messages[this.id].length) { 750 | var error = $('
  • '); 751 | if (data.settings.encodeErrorSummary) { 752 | error.text(messages[this.id][0]); 753 | } else { 754 | error.html(messages[this.id][0]); 755 | } 756 | $ul.append(error); 757 | } 758 | }); 759 | $summary.toggle($ul.find('li').length > 0); 760 | } 761 | }; 762 | 763 | var getValue = function ($form, attribute) { 764 | var $input = findInput($form, attribute); 765 | var type = $input.attr('type'); 766 | if (type === 'checkbox' || type === 'radio') { 767 | var $realInput = $input.filter(':checked'); 768 | if (!$realInput.length) { 769 | $realInput = $form.find('input[type=hidden][name="' + $input.attr('name') + '"]'); 770 | } 771 | 772 | return $realInput.val(); 773 | } else { 774 | return $input.val(); 775 | } 776 | }; 777 | 778 | var findInput = function ($form, attribute) { 779 | var $input = $form.find(attribute.input); 780 | if ($input.length && $input[0].tagName.toLowerCase() === 'div') { 781 | // checkbox list or radio list 782 | return $input.find('input'); 783 | } else { 784 | return $input; 785 | } 786 | }; 787 | 788 | var updateAriaInvalid = function ($form, attribute, hasError) { 789 | if (attribute.updateAriaInvalid) { 790 | $form.find(attribute.input).attr('aria-invalid', hasError ? 'true' : 'false'); 791 | } 792 | } 793 | })(window.jQuery); 794 | --------------------------------------------------------------------------------