diff --git a/apps/advanced/README.md b/apps/advanced/README.md index a2bcdd4..c443c90 100644 --- a/apps/advanced/README.md +++ b/apps/advanced/README.md @@ -67,32 +67,32 @@ If you do not have [Composer](http://getcomposer.org/), you may download it from curl -s http://getcomposer.org/installer | php ~~~ -You can then install the Bootstrap Application using the following command: +You can then install the application using the following command: ~~~ php composer.phar create-project --stability=dev yiisoft/yii2-app-advanced yii-advanced ~~~ -Now you should be able to access: - -- the frontend using the URL `http://localhost/yii-advanced/frontend/www/` -- the backstage using the URL `http://localhost/yii-advanced/backstage/www/` - -assuming `yii-advanced` is directly under the document root of your Web server. - ### Install from an Archive File This is not currently available. We will provide it when Yii 2 is formally released. + GETTING STARTED --------------- -After template application and its dependencies are downloaded you need to initialize it and set some config values to -match your application requirements. +After you install the application, you have to conduct the following steps to initialize +the installed application. You only need to do these once for all. -1. Execute `install` command selecting `dev` as environment. -2. Set `id` value in `console/config/main.php`, `frontend/config/main.php`, `backstage/config/main.php`. -3. Create new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`. -4. In `common/config/params.php` set your database details in `components.db` values. +1. Execute the `init` command and select `dev` as environment. +2. Create a new database. It is assumed that MySQL InnoDB is used. If not, adjust `console/migrations/m130524_201442_init.php`. +3. In `common/config/params.php` set your database details in `components.db` values. + +Now you should be able to access: + +- the frontend using the URL `http://localhost/yii-advanced/frontend/www/` +- the backstage using the URL `http://localhost/yii-advanced/backstage/www/` + +assuming `yii-advanced` is directly under the document root of your Web server. diff --git a/apps/advanced/backstage/config/main.php b/apps/advanced/backstage/config/main.php index 4898bfd..6e55c47 100644 --- a/apps/advanced/backstage/config/main.php +++ b/apps/advanced/backstage/config/main.php @@ -9,7 +9,7 @@ $params = array_merge( ); return array( - 'id' => 'change-me', + 'id' => 'app-backend', 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'preload' => array('log'), diff --git a/apps/advanced/composer.json b/apps/advanced/composer.json index db97efd..95b3b13 100644 --- a/apps/advanced/composer.json +++ b/apps/advanced/composer.json @@ -36,9 +36,6 @@ "frontend/runtime", "frontend/www/assets" - ], - "yii-install-executable": [ - "yii" ] } } diff --git a/apps/advanced/console/config/main.php b/apps/advanced/console/config/main.php index cceb311..37db1d2 100644 --- a/apps/advanced/console/config/main.php +++ b/apps/advanced/console/config/main.php @@ -9,7 +9,7 @@ $params = array_merge( ); return array( - 'id' => 'change-me', + 'id' => 'app-console', 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'preload' => array('log'), diff --git a/apps/advanced/console/migrations/m130524_201442_init.php b/apps/advanced/console/migrations/m130524_201442_init.php index 24a74c3..a5e9d30 100644 --- a/apps/advanced/console/migrations/m130524_201442_init.php +++ b/apps/advanced/console/migrations/m130524_201442_init.php @@ -1,5 +1,7 @@ 'change-me', + 'id' => 'app-frontend', 'basePath' => dirname(__DIR__), 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'preload' => array('log'), diff --git a/apps/advanced/install b/apps/advanced/init similarity index 87% rename from apps/advanced/install rename to apps/advanced/init index 6864440..17ed854 100755 --- a/apps/advanced/install +++ b/apps/advanced/init @@ -4,27 +4,27 @@ $root = str_replace('\\', '/', __DIR__); $envs = require("$root/environments/index.php"); $envNames = array_keys($envs); -echo "Yii Application Installation Tool v1.0\n\n"; -echo "Which environment do you want to install the application to?\n\n"; +echo "Yii Application Init Tool v1.0\n\n"; +echo "Which environment do you want the application to be initialized in?\n\n"; foreach ($envNames as $i => $name) { echo " [$i] $name\n"; } echo "\n Your choice [0-" . (count($envs) - 1) . ', or "q" to quit] '; $answer = trim(fgets(STDIN)); if (!ctype_digit($answer) || !isset($envNames[$answer])) { - echo "\n Quit installation.\n"; + echo "\n Quit initialization.\n"; return; } $env = $envs[$envNames[$answer]]; -echo "\n Install the application under '{$envNames[$answer]}' environment? [yes|no] "; +echo "\n Initialize the application under '{$envNames[$answer]}' environment? [yes|no] "; $answer = trim(fgets(STDIN)); if (strncasecmp($answer, 'y', 1)) { - echo "\n Quit installation.\n"; + echo "\n Quit initialization.\n"; return; } -echo "\n Start installation ...\n\n"; +echo "\n Start initialization ...\n\n"; $files = getFileList("$root/environments/{$env['path']}"); $all = false; foreach ($files as $file) { @@ -47,7 +47,7 @@ if (isset($env['executable'])) { } } -echo "\n ... installation completed.\n\n"; +echo "\n ... initialization completed.\n\n"; function getFileList($root, $basePath = '') { diff --git a/apps/advanced/install.bat b/apps/advanced/init.bat similarity index 100% rename from apps/advanced/install.bat rename to apps/advanced/init.bat diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index b3d4411..ebfe94b 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -13,7 +13,7 @@ The most obvious change in Yii 2.0 is the use of namespaces. Almost every core c is namespaced, e.g., `yii\web\Request`. The "C" prefix is no longer used in class names. The naming of the namespaces follows the directory structure. For example, `yii\web\Request` indicates the corresponding class file is `web/Request.php` under the Yii framework folder. -You can use any core class without explicitly include that class file, thanks to the Yii +You can use any core class without explicitly including that class file, thanks to the Yii class loader. @@ -117,17 +117,17 @@ supported in most places in the Yii core code. For example, `FileCache::cachePat both a path alias and a normal directory path. Path alias is also closely related with class namespaces. It is recommended that a path -alias defined for each root namespace so that you can use Yii class autoloader without +alias be defined for each root namespace so that you can use Yii the class autoloader without any further configuration. For example, because `@yii` refers to the Yii installation directory, a class like `yii\web\Request` can be autoloaded by Yii. If you use a third party library -such as Zend Framework, you may define a path alias `@Zend` which refers to its installation directory. -And Yii will be able to autoload any class in this library. +such as Zend Framework, you may define a path alias `@Zend` which refers to its installation +directory and Yii will be able to autoload any class in this library. View ---- -Yii 2.0 introduces a `View` class to represent the view part in the MVC pattern. +Yii 2.0 introduces a `View` class to represent the view part of the MVC pattern. It can be configured globally through the "view" application component. It is also accessible in any view file via `$this`. This is one of the biggest changes compared to 1.1: **`$this` in a view file no longer refers to the controller or widget object.** @@ -159,7 +159,7 @@ extension for your Smarty views, or `twig` for Twig views. You may also configur Models ------ -A model is now associated with a form name returned its `formName()` method. This is +A model is now associated with a form name returned by its `formName()` method. This is mainly used when using HTML forms to collect user inputs for a model. Previously in 1.1, this is usually hardcoded as the class name of the model. @@ -235,7 +235,7 @@ Previously in 1.1, you would have to enter the widget class names as strings via Themes ------ -Theme works completely different in 2.0. It is now based on a path map to "translate" a source +Themes work completely different in 2.0. They are now based on a path map to "translate" a source view into a themed view. For example, if the path map for a theme is `array('/www/views' => '/www/themes/basic')`, then the themed version for a view file `/www/views/site/index.php` will be `/www/themes/basic/site/index.php`. @@ -250,7 +250,7 @@ application component. Console Applications -------------------- -Console applications are now composed by controllers, too, like Web applications. In fact, +Console applications are now composed by controllers, like Web applications. In fact, console controllers and Web controllers share the same base controller class. Each console controller is like `CConsoleCommand` in 1.1. It consists of one or several @@ -300,7 +300,7 @@ public function behaviors() Assets ------ -Yii 2.0 introduces a new concept called *asset bundle*. It is a bit similar to script +Yii 2.0 introduces a new concept called *asset bundle*. It is similar to script packages (managed by `CClientScript`) in 1.1, but with better support. An asset bundle is a collection of asset files (e.g. JavaScript files, CSS files, image files, etc.) @@ -315,7 +315,7 @@ Static Helpers Yii 2.0 introduces many commonly used static helper classes, such as `Html`, `ArrayHelper`, `StringHelper`. These classes are designed to be easily extended. Note that static classes -are usually hard to be extended because of the fixed class name references. But Yii 2.0 +are usually hard to extend because of the fixed class name references. But Yii 2.0 introduces the class map (via `Yii::$classMap`) to overcome this difficulty. @@ -343,7 +343,7 @@ Query Builder In 1.1, query building is scattered among several classes, including `CDbCommand`, `CDbCriteria`, and `CDbCommandBuilder`. Yii 2.0 uses `Query` to represent a DB query -and `QueryBuilder` to generate SQL statements from query objects. For example, +and `QueryBuilder` to generate SQL statements from query objects. For example: ```php $query = new \yii\db\Query; @@ -365,7 +365,7 @@ ActiveRecord ------------ ActiveRecord has undergone significant changes in Yii 2.0. The most important one -is about relational ActiveRecord query. In 1.1, you have to declare the relations +is the relational ActiveRecord query. In 1.1, you have to declare the relations in the `relations()` method. In 2.0, this is done via getter methods that return an `ActiveQuery` object. For example, the following method declares an "orders" relation: @@ -392,7 +392,7 @@ by filtering with the primary keys of the primary records. Yii 2.0 no longer uses the `model()` method when performing queries. Instead, you -use the `find()` method like the following: +use the `find()` method: ```php // to retrieve all *active* customers and order them by their ID: @@ -410,14 +410,14 @@ Therefore, you can use all query methods of `Query`. Instead of returning ActiveRecord objects, you may call `ActiveQuery::asArray()` to return results in terms of arrays. This is more efficient and is especially useful -when you need to return large number of records. For example, +when you need to return a large number of records: ```php $customers = Customer::find()->asArray()->all(); ``` By default, ActiveRecord now only saves dirty attributes. In 1.1, all attributes -would be saved to database when you call `save()`, regardless they are changed or not, +are saved to database when you call `save()`, regardless of having changed or not, unless you explicitly list the attributes to save. @@ -427,7 +427,7 @@ Auto-quoting Table and Column Names Yii 2.0 supports automatic quoting of database table and column names. A name enclosed within double curly brackets is treated as a table name, and a name enclosed within double square brackets is treated as a column name. They will be quoted according to -the database driver being used. For example, +the database driver being used: ```php $command = $connection->createCommand('SELECT [[id]] FROM {{posts}}'); diff --git a/framework/yii/assets.php b/framework/yii/assets.php index 79fbeb5..63f7560 100644 --- a/framework/yii/assets.php +++ b/framework/yii/assets.php @@ -48,4 +48,11 @@ return array( YII_DEBUG ? 'punycode/punycode.js' : 'punycode/punycode.min.js', ), ), + 'yii/maskedinput' => array( + 'sourcePath' => __DIR__ . '/assets', + 'js' => array( + 'jquery.maskedinput.js', + ), + 'depends' => array('yii/jquery'), + ), ); diff --git a/framework/yii/assets/jquery.maskedinput.js b/framework/yii/assets/jquery.maskedinput.js new file mode 100644 index 0000000..49a5a72 --- /dev/null +++ b/framework/yii/assets/jquery.maskedinput.js @@ -0,0 +1,338 @@ +/* + Masked Input plugin for jQuery + Copyright (c) 2007-2013 Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: 1.3.1 +*/ +(function($) { + function getPasteEvent() { + var el = document.createElement('input'), + name = 'onpaste'; + el.setAttribute(name, ''); + return (typeof el[name] === 'function')?'paste':'input'; +} + +var pasteEventName = getPasteEvent() + ".mask", + ua = navigator.userAgent, + iPhone = /iphone/i.test(ua), + android=/android/i.test(ua), + caretTimeoutId; + +$.mask = { + //Predefined character definitions + definitions: { + '9': "[0-9]", + 'a': "[A-Za-z]", + '*': "[A-Za-z0-9]" + }, + dataName: "rawMaskFn", + placeholder: '_', +}; + +$.fn.extend({ + //Helper Function for Caret positioning + caret: function(begin, end) { + var range; + + if (this.length === 0 || this.is(":hidden")) { + return; + } + + if (typeof begin == 'number') { + end = (typeof end === 'number') ? end : begin; + return this.each(function() { + if (this.setSelectionRange) { + this.setSelectionRange(begin, end); + } else if (this.createTextRange) { + range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', begin); + range.select(); + } + }); + } else { + if (this[0].setSelectionRange) { + begin = this[0].selectionStart; + end = this[0].selectionEnd; + } else if (document.selection && document.selection.createRange) { + range = document.selection.createRange(); + begin = 0 - range.duplicate().moveStart('character', -100000); + end = begin + range.text.length; + } + return { begin: begin, end: end }; + } + }, + unmask: function() { + return this.trigger("unmask"); + }, + mask: function(mask, settings) { + var input, + defs, + tests, + partialPosition, + firstNonMaskPos, + len; + + if (!mask && this.length > 0) { + input = $(this[0]); + return input.data($.mask.dataName)(); + } + settings = $.extend({ + placeholder: $.mask.placeholder, // Load default placeholder + completed: null + }, settings); + + + defs = $.mask.definitions; + tests = []; + partialPosition = len = mask.length; + firstNonMaskPos = null; + + $.each(mask.split(""), function(i, c) { + if (c == '?') { + len--; + partialPosition = i; + } else if (defs[c]) { + tests.push(new RegExp(defs[c])); + if (firstNonMaskPos === null) { + firstNonMaskPos = tests.length - 1; + } + } else { + tests.push(null); + } + }); + + return this.trigger("unmask").each(function() { + var input = $(this), + buffer = $.map( + mask.split(""), + function(c, i) { + if (c != '?') { + return defs[c] ? settings.placeholder : c; + } + }), + focusText = input.val(); + + function seekNext(pos) { + while (++pos < len && !tests[pos]); + return pos; + } + + function seekPrev(pos) { + while (--pos >= 0 && !tests[pos]); + return pos; + } + + function shiftL(begin,end) { + var i, + j; + + if (begin<0) { + return; + } + + for (i = begin, j = seekNext(end); i < len; i++) { + if (tests[i]) { + if (j < len && tests[i].test(buffer[j])) { + buffer[i] = buffer[j]; + buffer[j] = settings.placeholder; + } else { + break; + } + + j = seekNext(j); + } + } + writeBuffer(); + input.caret(Math.max(firstNonMaskPos, begin)); + } + + function shiftR(pos) { + var i, + c, + j, + t; + + for (i = pos, c = settings.placeholder; i < len; i++) { + if (tests[i]) { + j = seekNext(i); + t = buffer[i]; + buffer[i] = c; + if (j < len && tests[j].test(t)) { + c = t; + } else { + break; + } + } + } + } + + function keydownEvent(e) { + var k = e.which, + pos, + begin, + end; + + //backspace, delete, and escape get special treatment + if (k === 8 || k === 46 || (iPhone && k === 127)) { + pos = input.caret(); + begin = pos.begin; + end = pos.end; + + if (end - begin === 0) { + begin=k!==46?seekPrev(begin):(end=seekNext(begin-1)); + end=k===46?seekNext(end):end; + } + clearBuffer(begin, end); + shiftL(begin, end - 1); + + e.preventDefault(); + } else if (k == 27) {//escape + input.val(focusText); + input.caret(0, checkVal()); + e.preventDefault(); + } + } + + function keypressEvent(e) { + var k = e.which, + pos = input.caret(), + p, + c, + next; + + if (e.ctrlKey || e.altKey || e.metaKey || k < 32) {//Ignore + return; + } else if (k) { + if (pos.end - pos.begin !== 0){ + clearBuffer(pos.begin, pos.end); + shiftL(pos.begin, pos.end-1); + } + + p = seekNext(pos.begin - 1); + if (p < len) { + c = String.fromCharCode(k); + if (tests[p].test(c)) { + shiftR(p); + + buffer[p] = c; + writeBuffer(); + next = seekNext(p); + + if(android){ + setTimeout($.proxy($.fn.caret,input,next),0); + }else{ + input.caret(next); + } + + if (settings.completed && next >= len) { + settings.completed.call(input); + } + } + } + e.preventDefault(); + } + } + + function clearBuffer(start, end) { + var i; + for (i = start; i < end && i < len; i++) { + if (tests[i]) { + buffer[i] = settings.placeholder; + } + } + } + + function writeBuffer() { input.val(buffer.join('')); } + + function checkVal(allow) { + //try to place characters where they belong + var test = input.val(), + lastMatch = -1, + i, + c; + + for (i = 0, pos = 0; i < len; i++) { + if (tests[i]) { + buffer[i] = settings.placeholder; + while (pos++ < test.length) { + c = test.charAt(pos - 1); + if (tests[i].test(c)) { + buffer[i] = c; + lastMatch = i; + break; + } + } + if (pos > test.length) { + break; + } + } else if (buffer[i] === test.charAt(pos) && i !== partialPosition) { + pos++; + lastMatch = i; + } + } + if (allow) { + writeBuffer(); + } else if (lastMatch + 1 < partialPosition) { + input.val(""); + clearBuffer(0, len); + } else { + writeBuffer(); + input.val(input.val().substring(0, lastMatch + 1)); + } + return (partialPosition ? i : firstNonMaskPos); + } + + input.data($.mask.dataName,function(){ + return $.map(buffer, function(c, i) { + return tests[i]&&c!=settings.placeholder ? c : null; + }).join(''); + }); + + if (!input.attr("readonly")) + input + .one("unmask", function() { + input + .unbind(".mask") + .removeData($.mask.dataName); + }) + .bind("focus.mask", function() { + clearTimeout(caretTimeoutId); + var pos, + moveCaret; + + focusText = input.val(); + pos = checkVal(); + + caretTimeoutId = setTimeout(function(){ + writeBuffer(); + if (pos == mask.length) { + input.caret(0, pos); + } else { + input.caret(pos); + } + }, 10); + }) + .bind("blur.mask", function() { + checkVal(); + if (input.val() != focusText) + input.change(); + }) + .bind("keydown.mask", keydownEvent) + .bind("keypress.mask", keypressEvent) + .bind(pasteEventName, function() { + setTimeout(function() { + var pos=checkVal(true); + input.caret(pos); + if (settings.completed && pos == input.val().length) + settings.completed.call(input); + }, 0); + }); + checkVal(); //Perform initial check for existing values + }); + } +}); + + +})(jQuery); \ No newline at end of file diff --git a/framework/yii/base/ActionFilter.php b/framework/yii/base/ActionFilter.php index d69c0fe..20ff142 100644 --- a/framework/yii/base/ActionFilter.php +++ b/framework/yii/base/ActionFilter.php @@ -30,8 +30,8 @@ class ActionFilter extends Behavior public function events() { return array( - 'beforeAction' => 'beforeFilter', - 'afterAction' => 'afterFilter', + Controller::EVENT_BEFORE_ACTION => 'beforeFilter', + Controller::EVENT_AFTER_ACTION => 'afterFilter', ); } diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php index eb0a0d3..d38f6a9 100644 --- a/framework/yii/base/Application.php +++ b/framework/yii/base/Application.php @@ -279,6 +279,15 @@ class Application extends Module } /** + * Returns the formatter component. + * @return \yii\base\Formatter the formatter application component. + */ + public function getFormatter() + { + return $this->getComponent('formatter'); + } + + /** * Returns the request component. * @return \yii\web\Request|\yii\console\Request the request component */ @@ -333,6 +342,9 @@ class Application extends Module 'errorHandler' => array( 'class' => 'yii\base\ErrorHandler', ), + 'formatter' => array( + 'class' => 'yii\base\Formatter', + ), 'i18n' => array( 'class' => 'yii\i18n\I18N', ), diff --git a/framework/yii/base/Controller.php b/framework/yii/base/Controller.php index 543605f..af33b63 100644 --- a/framework/yii/base/Controller.php +++ b/framework/yii/base/Controller.php @@ -151,43 +151,13 @@ class Controller extends Component /** * Binds the parameters to the action. * This method is invoked by [[Action]] when it begins to run with the given parameters. - * This method will check the parameter names that the action requires and return - * the provided parameters according to the requirement. If there is any missing parameter, - * an exception will be thrown. * @param Action $action the action to be bound with parameters * @param array $params the parameters to be bound to the action * @return array the valid parameters that the action can run with. - * @throws InvalidRequestException if there are missing parameters. */ public function bindActionParams($action, $params) { - if ($action instanceof InlineAction) { - $method = new \ReflectionMethod($this, $action->actionMethod); - } else { - $method = new \ReflectionMethod($action, 'run'); - } - - $args = array(); - $missing = array(); - foreach ($method->getParameters() as $param) { - $name = $param->getName(); - if (array_key_exists($name, $params)) { - $args[] = $params[$name]; - unset($params[$name]); - } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); - } else { - $missing[] = $name; - } - } - - if (!empty($missing)) { - throw new InvalidRequestException(Yii::t('yii', 'Missing required parameters: {params}', array( - '{params}' => implode(', ', $missing), - ))); - } - - return $args; + return array(); } /** @@ -272,18 +242,6 @@ class Controller extends Component } /** - * Validates the parameter being bound to actions. - * This method is invoked when parameters are being bound to the currently requested action. - * Child classes may override this method to throw exceptions when there are missing and/or unknown parameters. - * @param Action $action the currently requested action - * @param array $missingParams the names of the missing parameters - * @param array $unknownParams the unknown parameters (name => value) - */ - public function validateActionParams($action, $missingParams, $unknownParams) - { - } - - /** * @return string the controller ID that is prefixed with the module ID (if any). */ public function getUniqueId() diff --git a/framework/yii/base/Formatter.php b/framework/yii/base/Formatter.php new file mode 100644 index 0000000..d15e5f2 --- /dev/null +++ b/framework/yii/base/Formatter.php @@ -0,0 +1,292 @@ + + * @since 2.0 + */ +class Formatter extends Component +{ + /** + * @var string the default format string to be used to format a date using PHP date() function. + */ + public $dateFormat = 'Y/m/d'; + /** + * @var string the default format string to be used to format a time using PHP date() function. + */ + public $timeFormat = 'h:i:s A'; + /** + * @var string the default format string to be used to format a date and time using PHP date() function. + */ + public $datetimeFormat = 'Y/m/d h:i:s A'; + /** + * @var array the text to be displayed when formatting a boolean value. The first element corresponds + * to the text display for false, the second element for true. Defaults to array('No', 'Yes'). + */ + public $booleanFormat; + /** + * @var string the character displayed as the decimal point when formatting a number. + */ + public $decimalSeparator = '.'; + /** + * @var string the character displayed as the thousands separator character when formatting a number. + */ + public $thousandSeparator = ','; + + + /** + * Initializes the component. + */ + public function init() + { + if (empty($this->booleanFormat)) { + $this->booleanFormat = array(Yii::t('yii', 'No'), Yii::t('yii', 'Yes')); + } + } + + /** + * Formats the value as is without any formatting. + * This method simply returns back the parameter without any format. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asRaw($value) + { + return $value; + } + + /** + * Formats the value as an HTML-encoded plain text. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asText($value) + { + return Html::encode($value); + } + + /** + * Formats the value as an HTML-encoded plain text with newlines converted into breaks. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asNtext($value) + { + return nl2br(Html::encode($value)); + } + + /** + * Formats the value as HTML-encoded text paragraphs. + * Each text paragraph is enclosed within a `

` tag. + * One or multiple consecutive empty lines divide two paragraphs. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asParagraphs($value) + { + return str_replace('

', '', + '

' . preg_replace('/[\r\n]{2,}/', "

\n

", Html::encode($value)) . '

' + ); + } + + /** + * Formats the value as HTML text. + * The value will be purified using [[HtmlPurifier]] to avoid XSS attacks. + * Use [[asRaw()]] if you do not want any purification of the value. + * @param mixed $value the value to be formatted + * @param array|null $config the configuration for the HTMLPurifier class. + * @return string the formatted result + */ + public function asHtml($value, $config = null) + { + return HtmlPurifier::process($value, $config); + } + + /** + * Formats the value as a mailto link. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asEmail($value) + { + return Html::mailto($value); + } + + /** + * Formats the value as an image tag. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asImage($value) + { + return Html::img($value); + } + + /** + * Formats the value as a hyperlink. + * @param mixed $value the value to be formatted + * @return string the formatted result + */ + public function asUrl($value) + { + $url = $value; + if (strpos($url, 'http://') !== 0 && strpos($url, 'https://') !== 0) { + $url = 'http://' . $url; + } + return Html::a(Html::encode($value), $url); + } + + /** + * Formats the value as a boolean. + * @param mixed $value the value to be formatted + * @return string the formatted result + * @see booleanFormat + */ + public function asBoolean($value) + { + return $value ? $this->booleanFormat[1] : $this->booleanFormat[0]; + } + + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see dateFormat + */ + public function asDate($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->dateFormat : $format, $value); + } + + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[timeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see timeFormat + */ + public function asTime($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->timeFormat : $format, $value); + } + + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[datetimeFormat]] will be used. The format string should be one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + return date($format === null ? $this->datetimeFormat : $format, $value); + } + + /** + * Normalizes the given datetime value as one that can be taken by various date/time formatting methods. + * @param mixed $value the datetime value to be normalized. + * @return mixed the normalized datetime value + */ + protected function normalizeDatetimeValue($value) + { + if (is_string($value)) { + if (ctype_digit($value) || $value[0] === '-' && ctype_digit(substr($value, 1))) { + return (int)$value; + } else { + return strtotime($value); + } + } elseif ($value instanceof DateTime) { + return $value->getTimestamp(); + } else { + return (int)$value; + } + } + + /** + * Formats the value as an integer. + * @param mixed $value the value to be formatted + * @return string the formatting result. + */ + public function asInteger($value) + { + if (is_string($value) && preg_match('/^(-?\d+)/', $value, $matches)) { + return $matches[1]; + } else { + $value = (int)$value; + return "$value"; + } + } + + /** + * Formats the value as a double number. + * Property [[decimalSeparator]] will be used to represent the decimal point. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatting result. + * @see decimalSeparator + */ + public function asDouble($value, $decimals = 2) + { + return str_replace('.', $this->decimalSeparator, sprintf("%.{$decimals}f", $value)); + } + + /** + * Formats the value as a number with decimal and thousand separators. + * This method calls the PHP number_format() function to do the formatting. + * @param mixed $value the value to be formatted + * @param integer $decimals the number of digits after the decimal point + * @return string the formatted result + * @see decimalSeparator + * @see thousandSeparator + */ + public function asNumber($value, $decimals = 0) + { + return number_format($value, $decimals, $this->decimalSeparator, $this->thousandSeparator); + } +} diff --git a/framework/yii/base/InvalidRequestException.php b/framework/yii/base/InvalidRequestException.php deleted file mode 100644 index f4806ce..0000000 --- a/framework/yii/base/InvalidRequestException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @since 2.0 - */ -class InvalidRequestException extends UserException -{ - /** - * @return string the user-friendly name of this exception - */ - public function getName() - { - return \Yii::t('yii', 'Invalid Request'); - } -} - diff --git a/framework/yii/bootstrap/Dropdown.php b/framework/yii/bootstrap/Dropdown.php index 7a8cd51..8ba0eef 100644 --- a/framework/yii/bootstrap/Dropdown.php +++ b/framework/yii/bootstrap/Dropdown.php @@ -8,7 +8,7 @@ namespace yii\bootstrap; use yii\base\InvalidConfigException; -use yii\helpers\base\ArrayHelper; +use yii\helpers\ArrayHelper; use yii\helpers\Html; @@ -32,17 +32,22 @@ class Dropdown extends Widget * // optional, url of the item link * 'url' => '', * // optional the HTML attributes of the item link - * 'urlOptions'=> array(...), + * 'linkOptions'=> array(...), * // optional the HTML attributes of the item * 'options'=> array(...), * // optional, an array of items that configure a sub menu of the item * // note: if `items` is set, then `url` of the parent item will be ignored and automatically set to "#" + * // important: there is an issue with sub-dropdown menus, and as of 3.0, bootstrap won't support sub-dropdown + * // @see https://github.com/twitter/bootstrap/issues/5050#issuecomment-11741727 * 'items'=> array(...) * ) * ``` - * Additionally, you can also configure a dropdown item as string. */ public $items = array(); + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; /** @@ -60,43 +65,41 @@ class Dropdown extends Widget */ public function run() { - echo Html::beginTag('ul', $this->options) . "\n"; - echo $this->renderContents() . "\n"; - echo Html::endTag('ul') . "\n"; + echo $this->renderItems() . "\n"; $this->registerPlugin('dropdown'); } /** - * Renders dropdown contents as specified on [[items]]. + * Renders dropdown items as specified on [[items]]. * @return string the rendering result. * @throws InvalidConfigException */ - protected function renderContents() + protected function renderItems() { - $contents = array(); + $items = array(); foreach ($this->items as $item) { if (is_string($item)) { - $contents[] = $item; + $items[] = $item; continue; } if (!isset($item['label'])) { throw new InvalidConfigException("The 'label' option is required."); } - + $label = $this->encodeLabels ? Html::encode($item['label']) : $item['label']; $options = ArrayHelper::getValue($item, 'options', array()); - $urlOptions = ArrayHelper::getValue($item, 'urlOptions', array()); - $urlOptions['tabindex'] = '-1'; + $linkOptions = ArrayHelper::getValue($item, 'linkOptions', array()); + $linkOptions['tabindex'] = '-1'; if (isset($item['items'])) { $this->addCssClass($options, 'dropdown-submenu'); - $content = Html::a($item['label'], '#', $urlOptions) . $this->dropdown($item['items']); + $content = Html::a($label, '#', $linkOptions) . $this->dropdown($item['items']); } else { - $content = Html::a($item['label'], ArrayHelper::getValue($item, 'url', '#'), $urlOptions); + $content = Html::a($label, ArrayHelper::getValue($item, 'url', '#'), $linkOptions); } - $contents[] = Html::tag('li', $content , $options); + $items[] = Html::tag('li', $content , $options); } - return implode("\n", $contents); + return Html::tag('ul', implode("\n", $items), $this->options); } /** diff --git a/framework/yii/helpers/base/Html.php b/framework/yii/helpers/base/Html.php index 90396ec..9a0001c 100644 --- a/framework/yii/helpers/base/Html.php +++ b/framework/yii/helpers/base/Html.php @@ -344,7 +344,7 @@ class Html /** * Generates a hyperlink tag. * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * such as an image tag. If this is coming from end users, you should consider [[encode()]] * it to prevent XSS attacks. * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute @@ -366,7 +366,7 @@ class Html /** * Generates a mailto hyperlink. * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * such as an image tag. If this is coming from end users, you should consider [[encode()]] * it to prevent XSS attacks. * @param string $email email address. If this is null, the first parameter (link body) will be treated * as the email address and used. diff --git a/framework/yii/i18n/Formatter.php b/framework/yii/i18n/Formatter.php new file mode 100644 index 0000000..d688a15 --- /dev/null +++ b/framework/yii/i18n/Formatter.php @@ -0,0 +1,232 @@ + + * @since 2.0 + */ +class Formatter extends \yii\base\Formatter +{ + /** + * @var string the locale ID that is used to localize the date and number formatting. + * If not set, [[\yii\base\Application::language]] will be used. + */ + public $locale; + /** + * @var string the default format string to be used to format a date using PHP date() function. + */ + public $dateFormat = 'short'; + /** + * @var string the default format string to be used to format a time using PHP date() function. + */ + public $timeFormat = 'short'; + /** + * @var string the default format string to be used to format a date and time using PHP date() function. + */ + public $datetimeFormat = 'short'; + /** + * @var array the options to be set for the NumberFormatter objects. Please refer to + */ + public $numberFormatOptions = array(); + + + /** + * Initializes the component. + * This method will check if the "intl" PHP extension is installed and set the + * default value of [[locale]]. + * @throws InvalidConfigException if the "intl" PHP extension is not installed. + */ + public function init() + { + if (!extension_loaded('intl')) { + throw new InvalidConfigException('The "intl" PHP extension is not install. It is required to format data values in localized formats.'); + } + if ($this->locale === null) { + $this->locale = Yii::$app->language; + } + parent::init(); + } + + private $_dateFormats = array( + 'short' => IntlDateFormatter::SHORT, + 'medium' => IntlDateFormatter::MEDIUM, + 'long' => IntlDateFormatter::LONG, + 'full' => IntlDateFormatter::FULL, + ); + + /** + * Formats the value as a date. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be the one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see dateFormat + */ + public function asDate($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->dateFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a time. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be the one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see timeFormat + */ + public function asTime($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->timeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format]); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a datetime. + * @param integer|string|DateTime $value the value to be formatted. The following + * types of value are supported: + * + * - an integer representing a UNIX timestamp + * - a string that can be parsed into a UNIX timestamp via `strtotime()` + * - a PHP DateTime object + * + * @param string $format the format used to convert the value into a date string. + * If null, [[dateFormat]] will be used. The format string should be the one + * that can be recognized by the PHP `date()` function. + * @return string the formatted result + * @see datetimeFormat + */ + public function asDatetime($value, $format = null) + { + $value = $this->normalizeDatetimeValue($value); + if ($format === null) { + $format = $this->datetimeFormat; + } + if (isset($this->_dateFormats[$format])) { + $formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format]); + } else { + $formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE); + $formatter->setPattern($format); + } + return $formatter->format($value); + } + + /** + * Formats the value as a decimal number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asDecimal($value, $format = null) + { + return $this->createNumberFormatter(NumberFormatter::DECIMAL, $format)->format($value); + } + + /** + * Formats the value as a currency number. + * @param mixed $value the value to be formatted + * @param string $currency the 3-letter ISO 4217 currency code indicating the currency to use. + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asCurrency($value, $currency = 'USD', $format = null) + { + return $this->createNumberFormatter(NumberFormatter::CURRENCY, $format)->formatCurrency($value, $currency); + } + + /** + * Formats the value as a percent number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asPercent($value, $format = null) + { + return $this->createNumberFormatter(NumberFormatter::PERCENT, $format)->format($value); + } + + /** + * Formats the value as a scientific number. + * @param mixed $value the value to be formatted + * @param string $format the format to be used. Please refer to [ICU manual](http://www.icu-project.org/apiref/icu4c/classDecimalFormat.html#_details) + * for details on how to specify a format. + * @return string the formatted result. + */ + public function asScientific($value, $format = null) + { + return $this->createNumberFormatter(NumberFormatter::SCIENTIFIC, $format)->format($value); + } + + /** + * Creates a number formatter based on the given type and format. + * @param integer $type the type of the number formatter + * @param string $format the format to be used + * @return NumberFormatter the created formatter instance + */ + protected function createNumberFormatter($type, $format) + { + $formatter = new NumberFormatter($this->locale, $type); + if ($format !== null) { + $formatter->setPattern($format); + } + if (!empty($this->numberFormatOptions)) { + foreach ($this->numberFormatOptions as $name => $attribute) { + $formatter->setAttribute($name, $attribute); + } + } + return $formatter; + } +} diff --git a/framework/yii/requirements/requirements.php b/framework/yii/requirements/requirements.php index 0dbc1fc..63aa70d 100644 --- a/framework/yii/requirements/requirements.php +++ b/framework/yii/requirements/requirements.php @@ -43,6 +43,6 @@ return array( 'mandatory' => false, 'condition' => $this->checkPhpExtensionVersion('intl', '1.0.2'), 'by' => 'Internationalization support', - 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator.' + 'memo' => 'PHP Intl extension 1.0.2 or higher is required when you want to use IDN-feature of EmailValidator or UrlValidator or the yii\i18n\Formatter class.' ), ); \ No newline at end of file diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 517f4b4..026c078 100644 --- a/framework/yii/web/Controller.php +++ b/framework/yii/web/Controller.php @@ -8,6 +8,8 @@ namespace yii\web; use Yii; +use yii\base\HttpException; +use yii\base\InlineAction; /** * Controller is the base class of Web controllers. @@ -19,6 +21,48 @@ use Yii; class Controller extends \yii\base\Controller { /** + * Binds the parameters to the action. + * This method is invoked by [[Action]] when it begins to run with the given parameters. + * This method will check the parameter names that the action requires and return + * the provided parameters according to the requirement. If there is any missing parameter, + * an exception will be thrown. + * @param \yii\base\Action $action the action to be bound with parameters + * @param array $params the parameters to be bound to the action + * @return array the valid parameters that the action can run with. + * @throws HttpException if there are missing parameters. + */ + public function bindActionParams($action, $params) + { + if ($action instanceof InlineAction) { + $method = new \ReflectionMethod($this, $action->actionMethod); + } else { + $method = new \ReflectionMethod($action, 'run'); + } + + $args = array(); + $missing = array(); + foreach ($method->getParameters() as $param) { + $name = $param->getName(); + if (array_key_exists($name, $params)) { + $args[] = $params[$name]; + unset($params[$name]); + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } else { + $missing[] = $name; + } + } + + if (!empty($missing)) { + throw new HttpException(400, Yii::t('yii', 'Missing required parameters: {params}', array( + '{params}' => implode(', ', $missing), + ))); + } + + return $args; + } + + /** * Creates a URL using the given route and parameters. * * This method enhances [[UrlManager::createUrl()]] by supporting relative routes. diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php new file mode 100644 index 0000000..9b475e3 --- /dev/null +++ b/framework/yii/web/VerbFilter.php @@ -0,0 +1,90 @@ + array( + * 'class' => \yii\web\VerbFilter::className(), + * 'actions' => array( + * 'index' => array('get'), + * 'view' => array('get'), + * 'create' => array('get', 'post'), + * 'update' => array('get', 'put', 'post'), + * 'delete' => array('post', 'delete'), + * ), + * ), + * ); + * } + * ~~~ + * + * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + * @author Carsten Brandt + * @since 2.0 + */ +class VerbFilter extends Behavior +{ + /** + * @var array this property defines the allowed request methods for each action. + * For each action that should only support limited set of request methods + * you add an entry with the action id as array key and an array of + * allowed methods (e.g. GET, HEAD, PUT) as the value. + * If an action is not listed all request methods are considered allowed. + */ + public $actions = array(); + + + /** + * Declares event handlers for the [[owner]]'s events. + * @return array events (array keys) and the corresponding event handler methods (array values). + */ + public function events() + { + return array( + Controller::EVENT_BEFORE_ACTION => 'beforeAction', + ); + } + + /** + * @param ActionEvent $event + * @return boolean + * @throws \yii\base\HttpException when the request method is not allowed. + */ + public function beforeAction($event) + { + $action = $event->action->id; + if (isset($this->actions[$action])) { + $verb = Yii::$app->getRequest()->getRequestMethod(); + $allowed = array_map('strtoupper', $this->actions[$action]); + if (!in_array($verb, $allowed)) { + $event->isValid = false; + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 + header('Allow: ' . implode(', ', $allowed)); + throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed)); + } + } + return $event->isValid; + } +} diff --git a/framework/yii/widgets/ActiveForm.php b/framework/yii/widgets/ActiveForm.php index 25a2054..eb14293 100644 --- a/framework/yii/widgets/ActiveForm.php +++ b/framework/yii/widgets/ActiveForm.php @@ -134,8 +134,8 @@ class ActiveForm extends Widget $id = $this->options['id']; $options = Json::encode($this->getClientOptions()); $attributes = Json::encode($this->attributes); - $this->view->registerAssetBundle('yii/form'); - $this->view->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); + $this->getView()->registerAssetBundle('yii/form'); + $this->getView()->registerJs("jQuery('#$id').yiiActiveForm($attributes, $options);"); } echo Html::endForm(); } diff --git a/framework/yii/widgets/InputWidget.php b/framework/yii/widgets/InputWidget.php new file mode 100644 index 0000000..e1981c9 --- /dev/null +++ b/framework/yii/widgets/InputWidget.php @@ -0,0 +1,64 @@ + + * @since 2.0 + */ +class InputWidget extends Widget +{ + /** + * @var Model the data model that this widget is associated with. + */ + public $model; + /** + * @var string the model attribute that this widget is associated with. + */ + public $attribute; + /** + * @var string the input name. This must be set if [[model]] and [[attribute]] are not set. + */ + public $name; + /** + * @var string the input value. + */ + public $value; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + if (!$this->hasModel() && $this->name === null) { + throw new InvalidConfigException("Either 'name' or 'model' and 'attribute' properties must be specified."); + } + parent::init(); + } + + /** + * @return boolean whether this widget is associated with a data model. + */ + protected function hasModel() + { + return $this->model instanceof Model && $this->attribute !== null; + } +} diff --git a/framework/yii/widgets/MaskedInput.php b/framework/yii/widgets/MaskedInput.php new file mode 100644 index 0000000..a5a23f5 --- /dev/null +++ b/framework/yii/widgets/MaskedInput.php @@ -0,0 +1,136 @@ + 'phone', + * 'mask' => '999-999-9999', + * )); + * ~~~ + * + * The masked text field is implemented based on the [jQuery masked input plugin](http://digitalbush.com/projects/masked-input-plugin). + * + * @author Qiang Xue + * @since 2.0 + */ +class MaskedInput extends InputWidget +{ + /** + * @var string the input mask (e.g. '99/99/9999' for date input). The following characters are predefined: + * + * - `a`: represents an alpha character (A-Z,a-z) + * - `9`: represents a numeric character (0-9) + * - `*`: represents an alphanumeric character (A-Z,a-z,0-9) + * - `?`: anything listed after '?' within the mask is considered optional user input + * + * Additional characters can be defined by specifying the [[charMap]] property. + */ + public $mask; + /** + * @var array the mapping between mask characters and the corresponding patterns. + * For example, `array('~' => '[+-]')` specifies that the '~' character expects '+' or '-' input. + * Defaults to null, meaning using the map as described in [[mask]]. + */ + public $charMap; + /** + * @var string the character prompting for user input. Defaults to underscore '_'. + */ + public $placeholder; + /** + * @var string a JavaScript function callback that will be invoked when user finishes the input. + */ + public $completed; + /** + * @var array the HTML attributes for the input tag. + */ + public $options = array(); + + + /** + * Initializes the widget. + * @throws InvalidConfigException if the "mask" property is not set. + */ + public function init() + { + parent::init(); + if (empty($this->mask)) { + throw new InvalidConfigException('The "mask" property must be set.'); + } + + if (!isset($this->options['id'])) { + $this->options['id'] = $this->hasModel() ? Html::getInputId($this->model, $this->attribute) : $this->getId(); + } + } + + /** + * Runs the widget. + */ + public function run() + { + if ($this->hasModel()) { + echo Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + echo Html::textInput($this->name, $this->value, $this->options); + } + $this->registerClientScript(); + } + + /** + * Registers the needed JavaScript. + */ + public function registerClientScript() + { + $options = $this->getClientOptions(); + $options = empty($options) ? '' : ',' . Json::encode($options); + $js = ''; + if (is_array($this->charMap) && !empty($this->charMap)) { + $js .= 'jQuery.mask.definitions=' . Json::encode($this->charMap) . ";\n"; + } + $id = $this->options['id']; + $js .= "jQuery(\"#{$id}\").mask(\"{$this->mask}\"{$options});"; + $this->getView()->registerAssetBundle('yii/maskedinput'); + $this->getView()->registerJs($js); + } + + /** + * @return array the options for the text field + */ + protected function getClientOptions() + { + $options = array(); + if ($this->placeholder !== null) { + $options['placeholder'] = $this->placeholder; + } + + if ($this->completed !== null) { + if ($this->completed instanceof JsExpression) { + $options['completed'] = $this->completed; + } else { + $options['completed'] = new JsExpression($this->completed); + } + } + + return $options; + } +} diff --git a/tests/unit/framework/base/FormatterTest.php b/tests/unit/framework/base/FormatterTest.php new file mode 100644 index 0000000..e9a909f --- /dev/null +++ b/tests/unit/framework/base/FormatterTest.php @@ -0,0 +1,179 @@ + + * @since 2.0 + */ +class FormatterTest extends TestCase +{ + /** + * @var Formatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + $this->mockApplication(); + $this->formatter = new Formatter(); + } + + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } + + public function testAsRaw() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = 123; + $this->assertSame($value, $this->formatter->asRaw($value)); + $value = '<>'; + $this->assertSame($value, $this->formatter->asRaw($value)); + } + + public function testAsText() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asText($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asText($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asText($value)); + } + + public function testAsNtext() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asNtext($value)); + $value = 123; + $this->assertSame("$value", $this->formatter->asNtext($value)); + $value = '<>'; + $this->assertSame('<>', $this->formatter->asNtext($value)); + $value = "123\n456"; + $this->assertSame("123
\n456", $this->formatter->asNtext($value)); + } + + public function testAsParagraphs() + { + $value = '123'; + $this->assertSame("

$value

", $this->formatter->asParagraphs($value)); + $value = 123; + $this->assertSame("

$value

", $this->formatter->asParagraphs($value)); + $value = '<>'; + $this->assertSame('

<>

', $this->formatter->asParagraphs($value)); + $value = "123\n456"; + $this->assertSame("

123\n456

", $this->formatter->asParagraphs($value)); + $value = "123\n\n456"; + $this->assertSame("

123

\n

456

", $this->formatter->asParagraphs($value)); + $value = "123\n\n\n456"; + $this->assertSame("

123

\n

456

", $this->formatter->asParagraphs($value)); + } + + public function testAsHtml() + { + // todo: dependency on HtmlPurifier + } + + public function testAsEmail() + { + $value = 'test@sample.com'; + $this->assertSame("$value", $this->formatter->asEmail($value)); + } + + public function testAsImage() + { + $value = 'http://sample.com/img.jpg'; + $this->assertSame("\"\"", $this->formatter->asImage($value)); + } + + public function testAsBoolean() + { + $value = true; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = false; + $this->assertSame('No', $this->formatter->asBoolean($value)); + $value = "111"; + $this->assertSame('Yes', $this->formatter->asBoolean($value)); + $value = ""; + $this->assertSame('No', $this->formatter->asBoolean($value)); + } + + public function testAsDate() + { + $value = time(); + $this->assertSame(date('Y/m/d', $value), $this->formatter->asDate($value)); + $this->assertSame(date('Y-m-d', $value), $this->formatter->asDate($value, 'Y-m-d')); + } + + public function testAsTime() + { + $value = time(); + $this->assertSame(date('h:i:s A', $value), $this->formatter->asTime($value)); + $this->assertSame(date('h:i:s', $value), $this->formatter->asTime($value, 'h:i:s')); + } + + public function testAsDatetime() + { + $value = time(); + $this->assertSame(date('Y/m/d h:i:s A', $value), $this->formatter->asDatetime($value)); + $this->assertSame(date('Y-m-d h:i:s', $value), $this->formatter->asDatetime($value, 'Y-m-d h:i:s')); + } + + public function testAsInteger() + { + $value = 123; + $this->assertSame("$value", $this->formatter->asInteger($value)); + $value = 123.23; + $this->assertSame("123", $this->formatter->asInteger($value)); + $value = 'a'; + $this->assertSame("0", $this->formatter->asInteger($value)); + $value = -123.23; + $this->assertSame("-123", $this->formatter->asInteger($value)); + $value = "-123abc"; + $this->assertSame("-123", $this->formatter->asInteger($value)); + } + + public function testAsDouble() + { + $value = 123.12; + $this->assertSame("123.12", $this->formatter->asDouble($value)); + $this->assertSame("123.1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123; + $this->assertSame("123.00", $this->formatter->asDouble($value)); + $this->formatter->decimalSeparator = ','; + $value = 123.12; + $this->assertSame("123,12", $this->formatter->asDouble($value)); + $this->assertSame("123,1", $this->formatter->asDouble($value, 1)); + $this->assertSame("123", $this->formatter->asDouble($value, 0)); + $value = 123123.123; + $this->assertSame("123123,12", $this->formatter->asDouble($value)); + } + + public function testAsNumber() + { + $value = 123123.123; + $this->assertSame("123,123", $this->formatter->asNumber($value)); + $this->assertSame("123,123.12", $this->formatter->asNumber($value, 2)); + $this->formatter->decimalSeparator = ','; + $this->formatter->thousandSeparator = ' '; + $this->assertSame("123 123", $this->formatter->asNumber($value)); + $this->assertSame("123 123,12", $this->formatter->asNumber($value, 2)); + $this->formatter->thousandSeparator = ''; + $this->assertSame("123123", $this->formatter->asNumber($value)); + $this->assertSame("123123,12", $this->formatter->asNumber($value, 2)); + } +} diff --git a/tests/unit/framework/i18n/FormatterTest.php b/tests/unit/framework/i18n/FormatterTest.php new file mode 100644 index 0000000..f742f22 --- /dev/null +++ b/tests/unit/framework/i18n/FormatterTest.php @@ -0,0 +1,88 @@ + + * @since 2.0 + */ +class FormatterTest extends TestCase +{ + /** + * @var Formatter + */ + protected $formatter; + + protected function setUp() + { + parent::setUp(); + if (!extension_loaded('intl')) { + $this->markTestSkipped('intl extension is required.'); + } + $this->mockApplication(); + $this->formatter = new Formatter(array( + 'locale' => 'en_US', + )); + } + + protected function tearDown() + { + parent::tearDown(); + $this->formatter = null; + } + + public function testAsDecimal() + { + $value = '123'; + $this->assertSame($value, $this->formatter->asDecimal($value)); + $value = '123456'; + $this->assertSame("123,456", $this->formatter->asDecimal($value)); + $value = '-123456.123'; + $this->assertSame("-123,456.123", $this->formatter->asDecimal($value)); + } + + public function testAsPercent() + { + $value = '123'; + $this->assertSame('12,300%', $this->formatter->asPercent($value)); + $value = '0.1234'; + $this->assertSame("12%", $this->formatter->asPercent($value)); + $value = '-0.009343'; + $this->assertSame("-1%", $this->formatter->asPercent($value)); + } + + public function testAsScientific() + { + $value = '123'; + $this->assertSame('1.23E2', $this->formatter->asScientific($value)); + $value = '123456'; + $this->assertSame("1.23456E5", $this->formatter->asScientific($value)); + $value = '-123456.123'; + $this->assertSame("-1.23456123E5", $this->formatter->asScientific($value)); + } + + public function testAsCurrency() + { + $value = '123'; + $this->assertSame('$123.00', $this->formatter->asCurrency($value)); + $value = '123.456'; + $this->assertSame("$123.46", $this->formatter->asCurrency($value)); + $value = '-123456.123'; + $this->assertSame("($123,456.12)", $this->formatter->asCurrency($value)); + } + + public function testDate() + { + $time = time(); + $this->assertSame(date('n/j/y', $time), $this->formatter->asDate($time)); + $this->assertSame(date('M j, Y', $time), $this->formatter->asDate($time, 'long')); + } +}