From ec490dcaa60481addcaca7889cc6cc49f7b956ad Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 17:14:34 +0100 Subject: [PATCH 001/109] made gridview plural rule compatible with ICU version < 4.8 issue #1072 --- framework/yii/widgets/BaseListView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/widgets/BaseListView.php b/framework/yii/widgets/BaseListView.php index ffbba38..ba1f6d4 100644 --- a/framework/yii/widgets/BaseListView.php +++ b/framework/yii/widgets/BaseListView.php @@ -139,7 +139,7 @@ abstract class BaseListView extends Widget $pageCount = $pagination->pageCount; if (($summaryContent = $this->summary) === null) { $summaryContent = '
' - . Yii::t('yii', 'Showing {totalCount, plural, =0{0} other{{begin, number, integer}-{end, number, integer}}} of {totalCount, number, integer} {totalCount, plural, one{item} other{items}}.') + . Yii::t('yii', 'Showing {totalCount, plural, zero{0} other{{begin, number, integer}-{end, number, integer}}} of {totalCount, number, integer} {totalCount, plural, one{item} other{items}}.') . '
'; } } else { From 34945b0b69011bc7cab684c7f7095d837892a0d4 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 18:27:20 +0100 Subject: [PATCH 002/109] added unit test to verify storing of null values http://www.yiiframework.com/forum/index.php/topic/48359-inserting-nulls/page__view__findpost__p__226019 --- tests/unit/data/ar/NullValues.php | 20 +++++++++++ tests/unit/data/cubrid.sql | 11 ++++++ tests/unit/data/mssql.sql | 10 ++++++ tests/unit/data/mysql.sql | 10 ++++++ tests/unit/data/postgres.sql | 10 ++++++ tests/unit/data/sqlite.sql | 9 +++++ tests/unit/framework/db/ActiveRecordTest.php | 51 ++++++++++++++++++++++++++++ 7 files changed, 121 insertions(+) create mode 100644 tests/unit/data/ar/NullValues.php diff --git a/tests/unit/data/ar/NullValues.php b/tests/unit/data/ar/NullValues.php new file mode 100644 index 0000000..e6aa3b9 --- /dev/null +++ b/tests/unit/data/ar/NullValues.php @@ -0,0 +1,20 @@ +all(); $this->assertEquals(0, count($customers)); } + + public function testStoreNull() + { + $record = new NullValues(); + $this->assertNull($record->var1); + $this->assertNull($record->var2); + $this->assertNull($record->var3); + $this->assertNull($record->stringcol); + + $record->id = 1; + + $record->var1 = 123; + $record->var2 = 456; + $record->var3 = 789; + $record->stringcol = 'hello!'; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertEquals(123, $record->var1); + $this->assertEquals(456, $record->var2); + $this->assertEquals(789, $record->var3); + $this->assertEquals('hello!', $record->stringcol); + + $record->var1 = null; + $record->var2 = null; + $record->var3 = null; + $record->stringcol = null; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertNull($record->var1); + $this->assertNull($record->var2); + $this->assertNull($record->var3); + $this->assertNull($record->stringcol); + + $record->var1 = 0; + $record->var2 = 0; + $record->var3 = 0; + $record->stringcol = ''; + + $record->save(false); + $this->assertTrue($record->refresh()); + + $this->assertEquals(0, $record->var1); + $this->assertEquals(0, $record->var2); + $this->assertEquals(0, $record->var3); + $this->assertEquals('', $record->stringcol); + } } From 06a91d62712632d0fa1d80e0e4e1a5043b3848cf Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Tue, 29 Oct 2013 19:10:47 +0100 Subject: [PATCH 003/109] fixed unit tests for cubrid and postgresql both do not support unsigned integers --- tests/unit/data/cubrid.sql | 4 ++-- tests/unit/data/postgres.sql | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/data/cubrid.sql b/tests/unit/data/cubrid.sql index a9d4815..905ebd2 100644 --- a/tests/unit/data/cubrid.sql +++ b/tests/unit/data/cubrid.sql @@ -63,8 +63,8 @@ CREATE TABLE `tbl_order_item` ( ); CREATE TABLE tbl_null_values ( - `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, - `var1` INT UNSIGNED NULL, + `id` INT(11) NOT NULL AUTO_INCREMENT, + `var1` INT NULL, `var2` INT NULL, `var3` INT DEFAULT NULL, `stringcol` VARCHAR (32) DEFAULT NULL, diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql index f86d0d2..f9ee192 100644 --- a/tests/unit/data/postgres.sql +++ b/tests/unit/data/postgres.sql @@ -56,8 +56,8 @@ CREATE TABLE tbl_order_item ( ); CREATE TABLE tbl_null_values ( - id INT UNSIGNED AUTOINCREMENT NOT NULL, - var1 INT UNSIGNED NULL, + id INT NOT NULL, + var1 INT NULL, var2 INT NULL, var3 INT DEFAULT NULL, stringcol VARCHAR(32) DEFAULT NULL, From 0ee120f5f939a4ad8b0e5b332b6818a078666ce4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Tue, 29 Oct 2013 23:17:50 -0400 Subject: [PATCH 004/109] refactored Component::off(). --- framework/yii/base/Component.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/framework/yii/base/Component.php b/framework/yii/base/Component.php index 9d5258a..1d26dd7 100644 --- a/framework/yii/base/Component.php +++ b/framework/yii/base/Component.php @@ -406,24 +406,25 @@ class Component extends Object public function off($name, $handler = null) { $this->ensureBehaviors(); - if (isset($this->_events[$name])) { - if ($handler === null) { - $this->_events[$name] = []; - } else { - $removed = false; - foreach ($this->_events[$name] as $i => $event) { - if ($event[0] === $handler) { - unset($this->_events[$name][$i]); - $removed = true; - } - } - if ($removed) { - $this->_events[$name] = array_values($this->_events[$name]); + if (empty($this->_events[$name])) { + return false; + } + if ($handler === null) { + unset($this->_events[$name]); + return true; + } else { + $removed = false; + foreach ($this->_events[$name] as $i => $event) { + if ($event[0] === $handler) { + unset($this->_events[$name][$i]); + $removed = true; } - return $removed; } + if ($removed) { + $this->_events[$name] = array_values($this->_events[$name]); + } + return $removed; } - return false; } /** From 8b00693a0a4e13ea07623f7a96f57d68bffbe416 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 30 Oct 2013 00:12:48 -0400 Subject: [PATCH 005/109] Fixes #1025: Implemented support for class-level events. --- framework/yii/base/Component.php | 7 +- framework/yii/base/Event.php | 132 ++++++++++++++++++++++++++++ tests/unit/framework/base/ComponentTest.php | 1 - tests/unit/framework/base/EventTest.php | 93 ++++++++++++++++++++ 4 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 tests/unit/framework/base/EventTest.php diff --git a/framework/yii/base/Component.php b/framework/yii/base/Component.php index 1d26dd7..b67a09f 100644 --- a/framework/yii/base/Component.php +++ b/framework/yii/base/Component.php @@ -358,13 +358,13 @@ class Component extends Object public function hasEventHandlers($name) { $this->ensureBehaviors(); - return !empty($this->_events[$name]); + return !empty($this->_events[$name]) || Event::hasHandlers($this, $name); } /** * Attaches an event handler to an event. * - * An event handler must be a valid PHP callback. The followings are + * The event handler must be a valid PHP callback. The followings are * some examples: * * ~~~ @@ -374,7 +374,7 @@ class Component extends Object * 'handleClick' // global function handleClick() * ~~~ * - * An event handler must be defined with the following signature, + * The event handler must be defined with the following signature, * * ~~~ * function ($event) @@ -455,6 +455,7 @@ class Component extends Object } } } + Event::trigger($this, $name, $event); } /** diff --git a/framework/yii/base/Event.php b/framework/yii/base/Event.php index 5d40736..97ef905 100644 --- a/framework/yii/base/Event.php +++ b/framework/yii/base/Event.php @@ -45,4 +45,136 @@ class Event extends Object * Note that this varies according to which event handler is currently executing. */ public $data; + + private static $_events = []; + + /** + * Attaches an event handler to a class-level event. + * + * When a class-level event is triggered, event handlers attached + * to that class and all parent classes will be invoked. + * + * For example, the following code attaches an event handler to `ActiveRecord`'s + * `afterInsert` event: + * + * ~~~ + * Event::on([ActiveRecord::className, ActiveRecord::EVENT_AFTER_INSERT], function ($event) { + * Yii::trace(get_class($event->sender) . ' is inserted.'); + * }); + * ~~~ + * + * The handler will be invoked for EVERY successful ActiveRecord insertion. + * + * For more details about how to declare an event handler, please refer to [[Component::on()]]. + * + * @param string $class the fully qualified class name to which the event handler needs to attach + * @param string $name the event name + * @param callback $handler the event handler + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. + * @see off() + */ + public static function on($class, $name, $handler, $data = null) + { + self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data]; + } + + /** + * Detaches an event handler from a class-level event. + * + * This method is the opposite of [[on()]]. + * + * @param string $class the fully qualified class name from which the event handler needs to be detached + * @param string $name the event name + * @param callback $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. + * @return boolean if a handler is found and detached + * @see on() + */ + public static function off($class, $name, $handler = null) + { + $class = ltrim($class, '\\'); + if (empty(self::$_events[$name][$class])) { + return false; + } + if ($handler === null) { + unset(self::$_events[$name][$class]); + return true; + } else { + $removed = false; + foreach (self::$_events[$name][$class] as $i => $event) { + if ($event[0] === $handler) { + unset(self::$_events[$name][$class][$i]); + $removed = true; + } + } + if ($removed) { + self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); + } + return $removed; + } + } + + /** + * Returns a value indicating whether there is any handler attached to the specified class-level event. + * Note that this method will also check all parent classes to see if there is any handler attached + * to the named event. + * @param string|object $class the object or the fully qualified class name specifying the class-level event + * @param string $name the event name + * @return boolean whether there is any handler attached to the event. + */ + public static function hasHandlers($class, $name) + { + if (empty(self::$_events[$name])) { + return false; + } + if (is_object($class)) { + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + return true; + } + } while (($class = get_parent_class($class)) !== false); + return false; + } + + /** + * Triggers a class-level event. + * This method will cause invocation of event handlers that are attached to the named event + * for the specified class and all its parent classes. + * @param string|object $class the object or the fully qualified class name specifying the class-level event + * @param string $name the event name + * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. + */ + public static function trigger($class, $name, $event = null) + { + if (empty(self::$_events[$name])) { + return; + } + if ($event === null) { + $event = new self; + } + $event->handled = false; + $event->name = $name; + + if (is_object($class)) { + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + foreach (self::$_events[$name][$class] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); + if ($event instanceof Event && $event->handled) { + return; + } + } + } + } while (($class = get_parent_class($class)) !== false); + } } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index d1698eb..2cad56d 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -302,7 +302,6 @@ class ComponentTest extends TestCase $component->detachBehaviors(); $this->assertNull($component->getBehavior('a')); $this->assertNull($component->getBehavior('b')); - } } diff --git a/tests/unit/framework/base/EventTest.php b/tests/unit/framework/base/EventTest.php new file mode 100644 index 0000000..6226793 --- /dev/null +++ b/tests/unit/framework/base/EventTest.php @@ -0,0 +1,93 @@ + + * @since 2.0 + */ +class EventTest extends TestCase +{ + public $counter; + + public function setUp() + { + $this->counter = 0; + Event::off(ActiveRecord::className(), 'save'); + Event::off(Post::className(), 'save'); + Event::off(User::className(), 'save'); + } + + public function testOn() + { + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 3; + }); + $this->assertEquals(0, $this->counter); + $post = new Post; + $post->save(); + $this->assertEquals(4, $this->counter); + $user = new User; + $user->save(); + $this->assertEquals(7, $this->counter); + } + + public function testOff() + { + $handler = function ($event) { + $this->counter ++; + }; + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + Event::on(Post::className(), 'save', $handler); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + Event::off(Post::className(), 'save', $handler); + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + } + + public function testHasHandlers() + { + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + + $this->assertFalse(Event::hasHandlers(User::className(), 'save')); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(User::className(), 'save')); + $this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save')); + } +} + +class ActiveRecord extends Component +{ + public function save() + { + $this->trigger('save'); + } +} + +class Post extends ActiveRecord +{ +} + +class User extends ActiveRecord +{ + +} From 2880fb2899647de06bc0d0105cbd6792b5cc460f Mon Sep 17 00:00:00 2001 From: Luciano Baraglia Date: Wed, 30 Oct 2013 01:38:48 -0300 Subject: [PATCH 006/109] Typo [skip ci] --- framework/yii/base/Event.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/base/Event.php b/framework/yii/base/Event.php index 97ef905..02d12b5 100644 --- a/framework/yii/base/Event.php +++ b/framework/yii/base/Event.php @@ -58,7 +58,7 @@ class Event extends Object * `afterInsert` event: * * ~~~ - * Event::on([ActiveRecord::className, ActiveRecord::EVENT_AFTER_INSERT], function ($event) { + * Event::on([ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT], function ($event) { * Yii::trace(get_class($event->sender) . ' is inserted.'); * }); * ~~~ From 3920301c2b9843e2fa91a8e726ec913d5f8e4416 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 30 Oct 2013 12:14:19 +0400 Subject: [PATCH 007/109] Added note about "Inconsistent types declared for an argument: U_ARGUMENT_TYPE_MISMATCH" ICU error to i18n docs --- docs/guide/i18n.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index de1c80d..33f7758 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -232,6 +232,13 @@ for Russian: In the above it worth mentioning that `=1` matches exactly `n = 1` while `one` matches `21` or `101`. +Note that if you are using placeholder twice and one time it's used as plural another one should be used as number else +you'll get "Inconsistent types declared for an argument: U_ARGUMENT_TYPE_MISMATCH" error: + +``` +Total {count, number} {count, plural, one{item} other{items}}. +``` + To learn which inflection forms you should specify for your language you can referer to [rules reference at unicode.org](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). From 474d4aeaafaa42c854b6e8de10abd84c00cfa570 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 30 Oct 2013 12:44:54 +0400 Subject: [PATCH 008/109] Added info about class-level event handlers to doc --- docs/guide/upgrade-from-v1.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index 628cf54..4ccd726 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -106,6 +106,15 @@ Yii::$app->on($eventName, $handler); Yii::$app->trigger($eventName); ``` +If you need to handle all instances of a class instead of the object you can attach a handler like the following: + +```php +Event::on([ActiveRecord::className, ActiveRecord::EVENT_AFTER_INSERT], function ($event) { + Yii::trace(get_class($event->sender) . ' is inserted.'); +}); +``` + +The code above defines a handler that will be triggered for every Active Record object's `EVENT_AFTER_INSERT` event. Path Alias ---------- From 66fd16e8a6c9b0b99cda81bbd23c506916b59c69 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 30 Oct 2013 10:17:26 +0100 Subject: [PATCH 009/109] fixed gridview message pattern --- framework/yii/widgets/BaseListView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/widgets/BaseListView.php b/framework/yii/widgets/BaseListView.php index ba1f6d4..2ed27a2 100644 --- a/framework/yii/widgets/BaseListView.php +++ b/framework/yii/widgets/BaseListView.php @@ -146,7 +146,7 @@ abstract class BaseListView extends Widget $begin = $page = $pageCount = 1; $end = $totalCount = $count; if (($summaryContent = $this->summary) === null) { - $summaryContent = '
' . Yii::t('yii', 'Total {count} {count, plural, one{item} other{items}}.') . '
'; + $summaryContent = '
' . Yii::t('yii', 'Total {count, number, integer} {count, plural, one{item} other{items}}.') . '
'; } } return Yii::$app->getI18n()->format($summaryContent, [ From 6559b06ead48f58f89e23d1e958945391e687744 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Wed, 30 Oct 2013 11:20:03 +0100 Subject: [PATCH 010/109] made intl messages more compatible with various ICU versions issue #1072 --- framework/yii/i18n/MessageFormatter.php | 2 +- framework/yii/widgets/BaseListView.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/framework/yii/i18n/MessageFormatter.php b/framework/yii/i18n/MessageFormatter.php index 7abff83..f966344 100644 --- a/framework/yii/i18n/MessageFormatter.php +++ b/framework/yii/i18n/MessageFormatter.php @@ -92,7 +92,7 @@ class MessageFormatter extends Component return $this->fallbackFormat($pattern, $params, $language); } - if (version_compare(PHP_VERSION, '5.5.0', '<')) { + if (version_compare(PHP_VERSION, '5.5.0', '<') || version_compare(INTL_ICU_VERSION, '4.8', '<')) { $pattern = $this->replaceNamedArguments($pattern, $params); $params = array_values($params); } diff --git a/framework/yii/widgets/BaseListView.php b/framework/yii/widgets/BaseListView.php index 2ed27a2..310201a 100644 --- a/framework/yii/widgets/BaseListView.php +++ b/framework/yii/widgets/BaseListView.php @@ -135,18 +135,21 @@ abstract class BaseListView extends Widget $totalCount = $this->dataProvider->getTotalCount(); $begin = $pagination->getPage() * $pagination->pageSize + 1; $end = $begin + $count - 1; + if ($begin > $end) { + $begin = $end; + } $page = $pagination->getPage() + 1; $pageCount = $pagination->pageCount; if (($summaryContent = $this->summary) === null) { $summaryContent = '
' - . Yii::t('yii', 'Showing {totalCount, plural, zero{0} other{{begin, number, integer}-{end, number, integer}}} of {totalCount, number, integer} {totalCount, plural, one{item} other{items}}.') + . Yii::t('yii', 'Showing {begin, number}-{end, number} of {totalCount, number} {totalCount, plural, one{item} other{items}}.') . '
'; } } else { $begin = $page = $pageCount = 1; $end = $totalCount = $count; if (($summaryContent = $this->summary) === null) { - $summaryContent = '
' . Yii::t('yii', 'Total {count, number, integer} {count, plural, one{item} other{items}}.') . '
'; + $summaryContent = '
' . Yii::t('yii', 'Total {count, number} {count, plural, one{item} other{items}}.') . '
'; } } return Yii::$app->getI18n()->format($summaryContent, [ From 50ba760277dd984b7bb93c0f2d03ee2b4a853639 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 30 Oct 2013 10:33:02 -0400 Subject: [PATCH 011/109] Fixes #1106 --- framework/yii/gii/generators/crud/templates/views/_search.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/gii/generators/crud/templates/views/_search.php b/framework/yii/gii/generators/crud/templates/views/_search.php index ff9a0dd..03e21f2 100644 --- a/framework/yii/gii/generators/crud/templates/views/_search.php +++ b/framework/yii/gii/generators/crud/templates/views/_search.php @@ -34,7 +34,7 @@ foreach ($generator->getTableSchema()->getColumnNames() as $attribute) { if (++$count < 6) { echo "\t\tgenerateActiveSearchField($attribute) . " ?>\n\n"; } else { - echo "\t\tgenerateActiveSearchField($attribute) . " ?>\n\n"; + echo "\t\tgenerateActiveSearchField($attribute) . " ?>\n\n"; } } ?> From 4cb1a26477fab1a3c8ecc3f0a7ce808556f9b8c0 Mon Sep 17 00:00:00 2001 From: resurtm Date: Wed, 30 Oct 2013 21:56:02 +0600 Subject: [PATCH 012/109] Fixes #1104. Model generator table names auto complete. --- framework/yii/gii/Generator.php | 11 + framework/yii/gii/GiiAsset.php | 2 + framework/yii/gii/assets/main.css | 8 + framework/yii/gii/assets/typeahead.js | 1139 ++++++++++++++++++++ .../yii/gii/assets/typeahead.js-bootstrap.css | 51 + framework/yii/gii/components/ActiveField.php | 24 +- framework/yii/gii/generators/model/Generator.php | 12 + 7 files changed, 1246 insertions(+), 1 deletion(-) create mode 100644 framework/yii/gii/assets/typeahead.js create mode 100644 framework/yii/gii/assets/typeahead.js-bootstrap.css diff --git a/framework/yii/gii/Generator.php b/framework/yii/gii/Generator.php index 5c74567..83cdf6e 100644 --- a/framework/yii/gii/Generator.php +++ b/framework/yii/gii/Generator.php @@ -111,6 +111,17 @@ abstract class Generator extends Model } /** + * Returns the list of auto complete values. + * The array keys are the attribute names, and the array values are the corresponding auto complete values. + * Auto complete values can also be callable typed in order one want to make postponed data generation. + * @return array the list of auto complete values + */ + public function autoCompleteData() + { + return []; + } + + /** * Returns the message to be displayed when the newly generated code is saved successfully. * Child classes may override this method to customize the message. * @return string the message to be displayed when the newly generated code is saved successfully. diff --git a/framework/yii/gii/GiiAsset.php b/framework/yii/gii/GiiAsset.php index 26b6412..b100750 100644 --- a/framework/yii/gii/GiiAsset.php +++ b/framework/yii/gii/GiiAsset.php @@ -26,12 +26,14 @@ class GiiAsset extends AssetBundle */ public $css = [ 'main.css', + 'typeahead.js-bootstrap.css', ]; /** * @inheritdoc */ public $js = [ 'gii.js', + 'typeahead.js', ]; /** * @inheritdoc diff --git a/framework/yii/gii/assets/main.css b/framework/yii/gii/assets/main.css index 8efc56c..1a4f794 100644 --- a/framework/yii/gii/assets/main.css +++ b/framework/yii/gii/assets/main.css @@ -201,3 +201,11 @@ body { .DifferencesInline .ChangeReplace del { background: #e99; } + +/* additional styles for typeahead.js-bootstrap.css */ +.twitter-typeahead { + display: block !important; +} +.twitter-typeahead .tt-hint { + padding: 6px 12px !important; +} diff --git a/framework/yii/gii/assets/typeahead.js b/framework/yii/gii/assets/typeahead.js new file mode 100644 index 0000000..9365bd6 --- /dev/null +++ b/framework/yii/gii/assets/typeahead.js @@ -0,0 +1,1139 @@ +/*! + * typeahead.js 0.9.3 + * https://github.com/twitter/typeahead + * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT + */ + +(function($) { + var VERSION = "0.9.3"; + var utils = { + isMsie: function() { + var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); + return match ? parseInt(match[2], 10) : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + bind: $.proxy, + bindAll: function(obj) { + var val; + for (var key in obj) { + $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj)); + } + }, + indexOf: function(haystack, needle) { + for (var i = 0; i < haystack.length; i++) { + if (haystack[i] === needle) { + return i; + } + } + return -1; + }, + each: $.each, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + getUniqueId: function() { + var counter = 0; + return function() { + return counter++; + }; + }(), + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + tokenizeQuery: function(str) { + return $.trim(str).toLowerCase().split(/[\s]+/); + }, + tokenizeText: function(str) { + return $.trim(str).toLowerCase().split(/[\s\-_]+/); + }, + getProtocol: function() { + return location.protocol; + }, + noop: function() {} + }; + var EventTarget = function() { + var eventSplitter = /\s+/; + return { + on: function(events, callback) { + var event; + if (!callback) { + return this; + } + this._callbacks = this._callbacks || {}; + events = events.split(eventSplitter); + while (event = events.shift()) { + this._callbacks[event] = this._callbacks[event] || []; + this._callbacks[event].push(callback); + } + return this; + }, + trigger: function(events, data) { + var event, callbacks; + if (!this._callbacks) { + return this; + } + events = events.split(eventSplitter); + while (event = events.shift()) { + if (callbacks = this._callbacks[event]) { + for (var i = 0; i < callbacks.length; i += 1) { + callbacks[i].call(this, { + type: event, + data: data + }); + } + } + } + return this; + } + }; + }(); + var EventBus = function() { + var namespace = "typeahead:"; + function EventBus(o) { + if (!o || !o.el) { + $.error("EventBus initialized without el"); + } + this.$el = $(o.el); + } + utils.mixin(EventBus.prototype, { + trigger: function(type) { + var args = [].slice.call(arguments, 1); + this.$el.trigger(namespace + type, args); + } + }); + return EventBus; + }(); + var PersistentStorage = function() { + var ls, methods; + try { + ls = window.localStorage; + ls.setItem("~~~", "!"); + ls.removeItem("~~~"); + } catch (err) { + ls = null; + } + function PersistentStorage(namespace) { + this.prefix = [ "__", namespace, "__" ].join(""); + this.ttlKey = "__ttl__"; + this.keyMatcher = new RegExp("^" + this.prefix); + } + if (ls && window.JSON) { + methods = { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (utils.isNumber(ttl)) { + ls.setItem(this._ttlKey(key), encode(now() + ttl)); + } else { + ls.removeItem(this._ttlKey(key)); + } + return ls.setItem(this._prefix(key), encode(val)); + }, + remove: function(key) { + ls.removeItem(this._ttlKey(key)); + ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, key, keys = [], len = ls.length; + for (i = 0; i < len; i++) { + if ((key = ls.key(i)).match(this.keyMatcher)) { + keys.push(key.replace(this.keyMatcher, "")); + } + } + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(ls.getItem(this._ttlKey(key))); + return utils.isNumber(ttl) && now() > ttl ? true : false; + } + }; + } else { + methods = { + get: utils.noop, + set: utils.noop, + remove: utils.noop, + clear: utils.noop, + isExpired: utils.noop + }; + } + utils.mixin(PersistentStorage.prototype, methods); + return PersistentStorage; + function now() { + return new Date().getTime(); + } + function encode(val) { + return JSON.stringify(utils.isUndefined(val) ? null : val); + } + function decode(val) { + return JSON.parse(val); + } + }(); + var RequestCache = function() { + function RequestCache(o) { + utils.bindAll(this); + o = o || {}; + this.sizeLimit = o.sizeLimit || 10; + this.cache = {}; + this.cachedKeysByAge = []; + } + utils.mixin(RequestCache.prototype, { + get: function(url) { + return this.cache[url]; + }, + set: function(url, resp) { + var requestToEvict; + if (this.cachedKeysByAge.length === this.sizeLimit) { + requestToEvict = this.cachedKeysByAge.shift(); + delete this.cache[requestToEvict]; + } + this.cache[url] = resp; + this.cachedKeysByAge.push(url); + } + }); + return RequestCache; + }(); + var Transport = function() { + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache; + function Transport(o) { + utils.bindAll(this); + o = utils.isString(o) ? { + url: o + } : o; + requestCache = requestCache || new RequestCache(); + maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6; + this.url = o.url; + this.wildcard = o.wildcard || "%QUERY"; + this.filter = o.filter; + this.replace = o.replace; + this.ajaxSettings = { + type: "get", + cache: o.cache, + timeout: o.timeout, + dataType: o.dataType || "json", + beforeSend: o.beforeSend + }; + this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300); + } + utils.mixin(Transport.prototype, { + _get: function(url, cb) { + var that = this; + if (belowPendingRequestsThreshold()) { + this._sendRequest(url).done(done); + } else { + this.onDeckRequestArgs = [].slice.call(arguments, 0); + } + function done(resp) { + var data = that.filter ? that.filter(resp) : resp; + cb && cb(data); + requestCache.set(url, resp); + } + }, + _sendRequest: function(url) { + var that = this, jqXhr = pendingRequests[url]; + if (!jqXhr) { + incrementPendingRequests(); + jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always); + } + return jqXhr; + function always() { + decrementPendingRequests(); + pendingRequests[url] = null; + if (that.onDeckRequestArgs) { + that._get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; + } + } + }, + get: function(query, cb) { + var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp; + cb = cb || utils.noop; + url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery); + if (resp = requestCache.get(url)) { + utils.defer(function() { + cb(that.filter ? that.filter(resp) : resp); + }); + } else { + this._get(url, cb); + } + return !!resp; + } + }); + return Transport; + function incrementPendingRequests() { + pendingRequestsCount++; + } + function decrementPendingRequests() { + pendingRequestsCount--; + } + function belowPendingRequestsThreshold() { + return pendingRequestsCount < maxPendingRequests; + } + }(); + var Dataset = function() { + var keys = { + thumbprint: "thumbprint", + protocol: "protocol", + itemHash: "itemHash", + adjacencyList: "adjacencyList" + }; + function Dataset(o) { + utils.bindAll(this); + if (utils.isString(o.template) && !o.engine) { + $.error("no template engine specified"); + } + if (!o.local && !o.prefetch && !o.remote) { + $.error("one of local, prefetch, or remote is required"); + } + this.name = o.name || utils.getUniqueId(); + this.limit = o.limit || 5; + this.minLength = o.minLength || 1; + this.header = o.header; + this.footer = o.footer; + this.valueKey = o.valueKey || "value"; + this.template = compileTemplate(o.template, o.engine, this.valueKey); + this.local = o.local; + this.prefetch = o.prefetch; + this.remote = o.remote; + this.itemHash = {}; + this.adjacencyList = {}; + this.storage = o.name ? new PersistentStorage(o.name) : null; + } + utils.mixin(Dataset.prototype, { + _processLocalData: function(data) { + this._mergeProcessedData(this._processData(data)); + }, + _loadPrefetchData: function(o) { + var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred; + if (this.storage) { + storedThumbprint = this.storage.get(keys.thumbprint); + storedProtocol = this.storage.get(keys.protocol); + storedItemHash = this.storage.get(keys.itemHash); + storedAdjacencyList = this.storage.get(keys.adjacencyList); + } + isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol(); + o = utils.isString(o) ? { + url: o + } : o; + o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3; + if (storedItemHash && storedAdjacencyList && !isExpired) { + this._mergeProcessedData({ + itemHash: storedItemHash, + adjacencyList: storedAdjacencyList + }); + deferred = $.Deferred().resolve(); + } else { + deferred = $.getJSON(o.url).done(processPrefetchData); + } + return deferred; + function processPrefetchData(data) { + var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; + if (that.storage) { + that.storage.set(keys.itemHash, itemHash, o.ttl); + that.storage.set(keys.adjacencyList, adjacencyList, o.ttl); + that.storage.set(keys.thumbprint, thumbprint, o.ttl); + that.storage.set(keys.protocol, utils.getProtocol(), o.ttl); + } + that._mergeProcessedData(processedData); + } + }, + _transformDatum: function(datum) { + var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = { + value: value, + tokens: tokens + }; + if (utils.isString(datum)) { + item.datum = {}; + item.datum[this.valueKey] = datum; + } else { + item.datum = datum; + } + item.tokens = utils.filter(item.tokens, function(token) { + return !utils.isBlankString(token); + }); + item.tokens = utils.map(item.tokens, function(token) { + return token.toLowerCase(); + }); + return item; + }, + _processData: function(data) { + var that = this, itemHash = {}, adjacencyList = {}; + utils.each(data, function(i, datum) { + var item = that._transformDatum(datum), id = utils.getUniqueId(item.value); + itemHash[id] = item; + utils.each(item.tokens, function(i, token) { + var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]); + !~utils.indexOf(adjacency, id) && adjacency.push(id); + }); + }); + return { + itemHash: itemHash, + adjacencyList: adjacencyList + }; + }, + _mergeProcessedData: function(processedData) { + var that = this; + utils.mixin(this.itemHash, processedData.itemHash); + utils.each(processedData.adjacencyList, function(character, adjacency) { + var masterAdjacency = that.adjacencyList[character]; + that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency; + }); + }, + _getLocalSuggestions: function(terms) { + var that = this, firstChars = [], lists = [], shortestList, suggestions = []; + utils.each(terms, function(i, term) { + var firstChar = term.charAt(0); + !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar); + }); + utils.each(firstChars, function(i, firstChar) { + var list = that.adjacencyList[firstChar]; + if (!list) { + return false; + } + lists.push(list); + if (!shortestList || list.length < shortestList.length) { + shortestList = list; + } + }); + if (lists.length < firstChars.length) { + return []; + } + utils.each(shortestList, function(i, id) { + var item = that.itemHash[id], isCandidate, isMatch; + isCandidate = utils.every(lists, function(list) { + return ~utils.indexOf(list, id); + }); + isMatch = isCandidate && utils.every(terms, function(term) { + return utils.some(item.tokens, function(token) { + return token.indexOf(term) === 0; + }); + }); + isMatch && suggestions.push(item); + }); + return suggestions; + }, + initialize: function() { + var deferred; + this.local && this._processLocalData(this.local); + this.transport = this.remote ? new Transport(this.remote) : null; + deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve(); + this.local = this.prefetch = this.remote = null; + this.initialize = function() { + return deferred; + }; + return deferred; + }, + getSuggestions: function(query, cb) { + var that = this, terms, suggestions, cacheHit = false; + if (query.length < this.minLength) { + return; + } + terms = utils.tokenizeQuery(query); + suggestions = this._getLocalSuggestions(terms).slice(0, this.limit); + if (suggestions.length < this.limit && this.transport) { + cacheHit = this.transport.get(query, processRemoteData); + } + !cacheHit && cb && cb(suggestions); + function processRemoteData(data) { + suggestions = suggestions.slice(0); + utils.each(data, function(i, datum) { + var item = that._transformDatum(datum), isDuplicate; + isDuplicate = utils.some(suggestions, function(suggestion) { + return item.value === suggestion.value; + }); + !isDuplicate && suggestions.push(item); + return suggestions.length < that.limit; + }); + cb && cb(suggestions); + } + } + }); + return Dataset; + function compileTemplate(template, engine, valueKey) { + var renderFn, compiledTemplate; + if (utils.isFunction(template)) { + renderFn = template; + } else if (utils.isString(template)) { + compiledTemplate = engine.compile(template); + renderFn = utils.bind(compiledTemplate.render, compiledTemplate); + } else { + renderFn = function(context) { + return "

" + context[valueKey] + "

"; + }; + } + return renderFn; + } + }(); + var InputView = function() { + function InputView(o) { + var that = this; + utils.bindAll(this); + this.specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + this.$hint = $(o.hint); + this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent); + if (!utils.isMsie()) { + this.$input.on("input.tt", this._compareQueryToInputValue); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + utils.defer(that._compareQueryToInputValue); + }); + } + this.query = this.$input.val(); + this.$overflowHelper = buildOverflowHelper(this.$input); + } + utils.mixin(InputView.prototype, EventTarget, { + _handleFocus: function() { + this.trigger("focused"); + }, + _handleBlur: function() { + this.trigger("blured"); + }, + _handleSpecialKeyEvent: function($e) { + var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; + keyName && this.trigger(keyName + "Keyed", $e); + }, + _compareQueryToInputValue: function() { + var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false; + if (isSameQueryExceptWhitespace) { + this.trigger("whitespaceChanged", { + value: this.query + }); + } else if (!isSameQuery) { + this.trigger("queryChanged", { + value: this.query = inputValue + }); + } + }, + destroy: function() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$hint = this.$input = this.$overflowHelper = null; + }, + focus: function() { + this.$input.focus(); + }, + blur: function() { + this.$input.blur(); + }, + getQuery: function() { + return this.query; + }, + setQuery: function(query) { + this.query = query; + }, + getInputValue: function() { + return this.$input.val(); + }, + setInputValue: function(value, silent) { + this.$input.val(value); + !silent && this._compareQueryToInputValue(); + }, + getHintValue: function() { + return this.$hint.val(); + }, + setHintValue: function(value) { + this.$hint.val(value); + }, + getLanguageDirection: function() { + return (this.$input.css("direction") || "ltr").toLowerCase(); + }, + isOverflow: function() { + this.$overflowHelper.text(this.getInputValue()); + return this.$overflowHelper.width() > this.$input.width(); + }, + isCursorAtEnd: function() { + var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range; + if (utils.isNumber(selectionStart)) { + return selectionStart === valueLength; + } else if (document.selection) { + range = document.selection.createRange(); + range.moveStart("character", -valueLength); + return valueLength === range.text.length; + } + return true; + } + }); + return InputView; + function buildOverflowHelper($input) { + return $("").css({ + position: "absolute", + left: "-9999px", + visibility: "hidden", + whiteSpace: "nowrap", + fontFamily: $input.css("font-family"), + fontSize: $input.css("font-size"), + fontStyle: $input.css("font-style"), + fontVariant: $input.css("font-variant"), + fontWeight: $input.css("font-weight"), + wordSpacing: $input.css("word-spacing"), + letterSpacing: $input.css("letter-spacing"), + textIndent: $input.css("text-indent"), + textRendering: $input.css("text-rendering"), + textTransform: $input.css("text-transform") + }).insertAfter($input); + } + function compareQueries(a, b) { + a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + return a === b; + } + }(); + var DropdownView = function() { + var html = { + suggestionsList: '' + }, css = { + suggestionsList: { + display: "block" + }, + suggestion: { + whiteSpace: "nowrap", + cursor: "pointer" + }, + suggestionChild: { + whiteSpace: "normal" + } + }; + function DropdownView(o) { + utils.bindAll(this); + this.isOpen = false; + this.isEmpty = true; + this.isMouseOverDropdown = false; + this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover); + } + utils.mixin(DropdownView.prototype, EventTarget, { + _handleMouseenter: function() { + this.isMouseOverDropdown = true; + }, + _handleMouseleave: function() { + this.isMouseOverDropdown = false; + }, + _handleMouseover: function($e) { + var $suggestion = $($e.currentTarget); + this._getSuggestions().removeClass("tt-is-under-cursor"); + $suggestion.addClass("tt-is-under-cursor"); + }, + _handleSelection: function($e) { + var $suggestion = $($e.currentTarget); + this.trigger("suggestionSelected", extractSuggestion($suggestion)); + }, + _show: function() { + this.$menu.css("display", "block"); + }, + _hide: function() { + this.$menu.hide(); + }, + _moveCursor: function(increment) { + var $suggestions, $cur, nextIndex, $underCursor; + if (!this.isVisible()) { + return; + } + $suggestions = this._getSuggestions(); + $cur = $suggestions.filter(".tt-is-under-cursor"); + $cur.removeClass("tt-is-under-cursor"); + nextIndex = $suggestions.index($cur) + increment; + nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; + if (nextIndex === -1) { + this.trigger("cursorRemoved"); + return; + } else if (nextIndex < -1) { + nextIndex = $suggestions.length - 1; + } + $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor"); + this._ensureVisibility($underCursor); + this.trigger("cursorMoved", extractSuggestion($underCursor)); + }, + _getSuggestions: function() { + return this.$menu.find(".tt-suggestions > .tt-suggestion"); + }, + _ensureVisibility: function($el) { + var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true); + if (elTop < 0) { + this.$menu.scrollTop(menuScrollTop + elTop); + } else if (menuHeight < elBottom) { + this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); + } + }, + destroy: function() { + this.$menu.off(".tt"); + this.$menu = null; + }, + isVisible: function() { + return this.isOpen && !this.isEmpty; + }, + closeUnlessMouseIsOverDropdown: function() { + if (!this.isMouseOverDropdown) { + this.close(); + } + }, + close: function() { + if (this.isOpen) { + this.isOpen = false; + this.isMouseOverDropdown = false; + this._hide(); + this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"); + this.trigger("closed"); + } + }, + open: function() { + if (!this.isOpen) { + this.isOpen = true; + !this.isEmpty && this._show(); + this.trigger("opened"); + } + }, + setLanguageDirection: function(dir) { + var ltrCss = { + left: "0", + right: "auto" + }, rtlCss = { + left: "auto", + right: " 0" + }; + dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); + }, + moveCursorUp: function() { + this._moveCursor(-1); + }, + moveCursorDown: function() { + this._moveCursor(+1); + }, + getSuggestionUnderCursor: function() { + var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first(); + return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; + }, + getFirstSuggestion: function() { + var $suggestion = this._getSuggestions().first(); + return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; + }, + renderSuggestions: function(dataset, suggestions) { + var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '
%body
', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el; + if ($dataset.length === 0) { + $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); + $dataset = $("
").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu); + } + if (suggestions.length > 0) { + this.isEmpty = false; + this.isOpen && this._show(); + elBuilder = document.createElement("div"); + fragment = document.createDocumentFragment(); + utils.each(suggestions, function(i, suggestion) { + suggestion.dataset = dataset.name; + compiledHtml = dataset.template(suggestion.datum); + elBuilder.innerHTML = wrapper.replace("%body", compiledHtml); + $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion); + $el.children().each(function() { + $(this).css(css.suggestionChild); + }); + fragment.appendChild($el[0]); + }); + $dataset.show().find(".tt-suggestions").html(fragment); + } else { + this.clearSuggestions(dataset.name); + } + this.trigger("suggestionsRendered"); + }, + clearSuggestions: function(datasetName) { + var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions"); + $datasets.hide(); + $suggestions.empty(); + if (this._getSuggestions().length === 0) { + this.isEmpty = true; + this._hide(); + } + } + }); + return DropdownView; + function extractSuggestion($el) { + return $el.data("suggestion"); + } + }(); + var TypeaheadView = function() { + var html = { + wrapper: '', + hint: '', + dropdown: '' + }, css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none" + }, + query: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + dropdown: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + } + }; + if (utils.isMsie()) { + utils.mixin(css.query, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + if (utils.isMsie() && utils.isMsie() <= 7) { + utils.mixin(css.wrapper, { + display: "inline", + zoom: "1" + }); + utils.mixin(css.query, { + marginTop: "-1px" + }); + } + function TypeaheadView(o) { + var $menu, $input, $hint; + utils.bindAll(this); + this.$node = buildDomStructure(o.input); + this.datasets = o.datasets; + this.dir = null; + this.eventBus = o.eventBus; + $menu = this.$node.find(".tt-dropdown-menu"); + $input = this.$node.find(".tt-query"); + $hint = this.$node.find(".tt-hint"); + this.dropdownView = new DropdownView({ + menu: $menu + }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent); + this.inputView = new InputView({ + input: $input, + hint: $hint + }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete); + } + utils.mixin(TypeaheadView.prototype, EventTarget, { + _managePreventDefault: function(e) { + var $e = e.data, hint, inputValue, preventDefault = false; + switch (e.type) { + case "tabKeyed": + hint = this.inputView.getHintValue(); + inputValue = this.inputView.getInputValue(); + preventDefault = hint && hint !== inputValue; + break; + + case "upKeyed": + case "downKeyed": + preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; + break; + } + preventDefault && $e.preventDefault(); + }, + _setLanguageDirection: function() { + var dir = this.inputView.getLanguageDirection(); + if (dir !== this.dir) { + this.dir = dir; + this.$node.css("direction", dir); + this.dropdownView.setLanguageDirection(dir); + } + }, + _updateHint: function() { + var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match; + if (hint && dropdownIsVisible && !inputHasOverflow) { + inputValue = this.inputView.getInputValue(); + query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, ""); + escapedQuery = utils.escapeRegExChars(query); + beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); + match = beginsWithQuery.exec(hint); + this.inputView.setHintValue(inputValue + (match ? match[1] : "")); + } + }, + _clearHint: function() { + this.inputView.setHintValue(""); + }, + _clearSuggestions: function() { + this.dropdownView.clearSuggestions(); + }, + _setInputValueToQuery: function() { + this.inputView.setInputValue(this.inputView.getQuery()); + }, + _setInputValueToSuggestionUnderCursor: function(e) { + var suggestion = e.data; + this.inputView.setInputValue(suggestion.value, true); + }, + _openDropdown: function() { + this.dropdownView.open(); + }, + _closeDropdown: function(e) { + this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"](); + }, + _moveDropdownCursor: function(e) { + var $e = e.data; + if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { + this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"](); + } + }, + _handleSelection: function(e) { + var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); + if (suggestion) { + this.inputView.setInputValue(suggestion.value); + byClick ? this.inputView.focus() : e.data.preventDefault(); + byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close(); + this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset); + } + }, + _getSuggestions: function() { + var that = this, query = this.inputView.getQuery(); + if (utils.isBlankString(query)) { + return; + } + utils.each(this.datasets, function(i, dataset) { + dataset.getSuggestions(query, function(suggestions) { + if (query === that.inputView.getQuery()) { + that.dropdownView.renderSuggestions(dataset, suggestions); + } + }); + }); + }, + _autocomplete: function(e) { + var isCursorAtEnd, ignoreEvent, query, hint, suggestion; + if (e.type === "rightKeyed" || e.type === "leftKeyed") { + isCursorAtEnd = this.inputView.isCursorAtEnd(); + ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed"; + if (!isCursorAtEnd || ignoreEvent) { + return; + } + } + query = this.inputView.getQuery(); + hint = this.inputView.getHintValue(); + if (hint !== "" && query !== hint) { + suggestion = this.dropdownView.getFirstSuggestion(); + this.inputView.setInputValue(suggestion.value); + this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset); + } + }, + _propagateEvent: function(e) { + this.eventBus.trigger(e.type); + }, + destroy: function() { + this.inputView.destroy(); + this.dropdownView.destroy(); + destroyDomStructure(this.$node); + this.$node = null; + }, + setQuery: function(query) { + this.inputView.setQuery(query); + this.inputView.setInputValue(query); + this._clearHint(); + this._clearSuggestions(); + this._getSuggestions(); + } + }); + return TypeaheadView; + function buildDomStructure(input) { + var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint); + $wrapper = $wrapper.css(css.wrapper); + $dropdown = $dropdown.css(css.dropdown); + $hint.css(css.hint).css({ + backgroundAttachment: $input.css("background-attachment"), + backgroundClip: $input.css("background-clip"), + backgroundColor: $input.css("background-color"), + backgroundImage: $input.css("background-image"), + backgroundOrigin: $input.css("background-origin"), + backgroundPosition: $input.css("background-position"), + backgroundRepeat: $input.css("background-repeat"), + backgroundSize: $input.css("background-size") + }); + $input.data("ttAttrs", { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass("tt-query").attr({ + autocomplete: "off", + spellcheck: false + }).css(css.query); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input.wrap($wrapper).parent().prepend($hint).append($dropdown); + } + function destroyDomStructure($node) { + var $input = $node.find(".tt-query"); + utils.each($input.data("ttAttrs"), function(key, val) { + utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node); + $node.remove(); + } + }(); + (function() { + var cache = {}, viewKey = "ttView", methods; + methods = { + initialize: function(datasetDefs) { + var datasets; + datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ]; + if (datasetDefs.length === 0) { + $.error("no datasets provided"); + } + datasets = utils.map(datasetDefs, function(o) { + var dataset = cache[o.name] ? cache[o.name] : new Dataset(o); + if (o.name) { + cache[o.name] = dataset; + } + return dataset; + }); + return this.each(initialize); + function initialize() { + var $input = $(this), deferreds, eventBus = new EventBus({ + el: $input + }); + deferreds = utils.map(datasets, function(dataset) { + return dataset.initialize(); + }); + $input.data(viewKey, new TypeaheadView({ + input: $input, + eventBus: eventBus = new EventBus({ + el: $input + }), + datasets: datasets + })); + $.when.apply($, deferreds).always(function() { + utils.defer(function() { + eventBus.trigger("initialized"); + }); + }); + } + }, + destroy: function() { + return this.each(destroy); + function destroy() { + var $this = $(this), view = $this.data(viewKey); + if (view) { + view.destroy(); + $this.removeData(viewKey); + } + } + }, + setQuery: function(query) { + return this.each(setQuery); + function setQuery() { + var view = $(this).data(viewKey); + view && view.setQuery(query); + } + } + }; + jQuery.fn.typeahead = function(method) { + if (methods[method]) { + return methods[method].apply(this, [].slice.call(arguments, 1)); + } else { + return methods.initialize.apply(this, arguments); + } + }; + })(); +})(window.jQuery); \ No newline at end of file diff --git a/framework/yii/gii/assets/typeahead.js-bootstrap.css b/framework/yii/gii/assets/typeahead.js-bootstrap.css new file mode 100644 index 0000000..987aaf5 --- /dev/null +++ b/framework/yii/gii/assets/typeahead.js-bootstrap.css @@ -0,0 +1,51 @@ +/* always keep this link here when updating this file: https://github.com/jharding/typeahead.js-bootstrap.css */ + +.twitter-typeahead .tt-query, +.twitter-typeahead .tt-hint { + margin-bottom: 0; +} + +.tt-dropdown-menu { + min-width: 160px; + margin-top: 2px; + padding: 5px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0,0,0,.2); + *border-right-width: 2px; + *border-bottom-width: 2px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; +} + +.tt-suggestion { + display: block; + padding: 3px 20px; +} + +.tt-suggestion.tt-is-under-cursor { + color: #fff; + background-color: #0081c2; + background-image: -moz-linear-gradient(top, #0088cc, #0077b3); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3)); + background-image: -webkit-linear-gradient(top, #0088cc, #0077b3); + background-image: -o-linear-gradient(top, #0088cc, #0077b3); + background-image: linear-gradient(to bottom, #0088cc, #0077b3); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0) +} + +.tt-suggestion.tt-is-under-cursor a { + color: #fff; +} + +.tt-suggestion p { + margin: 0; +} diff --git a/framework/yii/gii/components/ActiveField.php b/framework/yii/gii/components/ActiveField.php index 8bb67a9..ae6f144 100644 --- a/framework/yii/gii/components/ActiveField.php +++ b/framework/yii/gii/components/ActiveField.php @@ -8,6 +8,7 @@ namespace yii\gii\components; use yii\gii\Generator; +use yii\helpers\Json; /** * @author Qiang Xue @@ -30,10 +31,18 @@ class ActiveField extends \yii\widgets\ActiveField if (isset($hints[$this->attribute])) { $this->hint($hints[$this->attribute]); } + $autoCompleteData = $this->model->autoCompleteData(); + if (isset($autoCompleteData[$this->attribute])) { + if (is_callable($autoCompleteData[$this->attribute])) { + $this->autoComplete(call_user_func($autoCompleteData[$this->attribute])); + } else { + $this->autoComplete($autoCompleteData[$this->attribute]); + } + } } /** - * Makes filed remember its value between page reloads + * Makes field remember its value between page reloads * @return static the field object itself */ public function sticky() @@ -41,4 +50,17 @@ class ActiveField extends \yii\widgets\ActiveField $this->options['class'] .= ' sticky'; return $this; } + + /** + * Makes field auto completable + * @param array $data auto complete data (array of callables or scalars) + * @return static the field object itself + */ + public function autoComplete($data) + { + static $counter = 0; + $this->inputOptions['class'] .= ' typeahead-' . (++$counter); + $this->form->getView()->registerJs("jQuery('.typeahead-{$counter}').typeahead({local: " . Json::encode($data) . "});"); + return $this; + } } diff --git a/framework/yii/gii/generators/model/Generator.php b/framework/yii/gii/generators/model/Generator.php index bd42fab..a7f1aa7 100644 --- a/framework/yii/gii/generators/model/Generator.php +++ b/framework/yii/gii/generators/model/Generator.php @@ -113,6 +113,18 @@ class Generator extends \yii\gii\Generator /** * @inheritdoc */ + public function autoCompleteData() + { + return [ + 'tableName' => function () { + return $this->getDbConnection()->getSchema()->getTableNames(); + }, + ]; + } + + /** + * @inheritdoc + */ public function requiredTemplates() { return ['model.php']; From f9a92b82df44475af381bbbdfe5a03d2b1871535 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 31 Oct 2013 00:48:37 +0400 Subject: [PATCH 013/109] fixed typo --- docs/guide/upgrade-from-v1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/upgrade-from-v1.md b/docs/guide/upgrade-from-v1.md index 4ccd726..2bf080a 100644 --- a/docs/guide/upgrade-from-v1.md +++ b/docs/guide/upgrade-from-v1.md @@ -109,7 +109,7 @@ Yii::$app->trigger($eventName); If you need to handle all instances of a class instead of the object you can attach a handler like the following: ```php -Event::on([ActiveRecord::className, ActiveRecord::EVENT_AFTER_INSERT], function ($event) { +Event::on([ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT], function ($event) { Yii::trace(get_class($event->sender) . ' is inserted.'); }); ``` From 2f360e53c39cfc4fa3bc28d15d12b560422c17f8 Mon Sep 17 00:00:00 2001 From: resurtm Date: Thu, 31 Oct 2013 17:55:44 +0600 Subject: [PATCH 014/109] Add CoffeeScript and TypeScript commands to AssetConverter. --- framework/yii/web/AssetConverter.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/framework/yii/web/AssetConverter.php b/framework/yii/web/AssetConverter.php index b7f406d..75a3106 100644 --- a/framework/yii/web/AssetConverter.php +++ b/framework/yii/web/AssetConverter.php @@ -28,6 +28,8 @@ class AssetConverter extends Component implements AssetConverterInterface 'scss' => ['css', 'sass {from} {to}'], 'sass' => ['css', 'sass {from} {to}'], 'styl' => ['js', 'stylus < {from} > {to}'], + 'coffee' => ['js', 'coffee -p {from} > {to}'], + 'ts' => ['js', 'tsc --out {to} {from}'], ]; /** From 7e805864d426e885aa4aa4ab717093344218961f Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Thu, 31 Oct 2013 23:55:33 +0400 Subject: [PATCH 015/109] added brief description of forms --- docs/guide/form.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/docs/guide/form.md b/docs/guide/form.md index c1f1ba3..c811c10 100644 --- a/docs/guide/form.md +++ b/docs/guide/form.md @@ -1,3 +1,96 @@ Working with forms ================== +The primary way of using forms in Yii is [[\yii\widgets\ActiveForm]]. It should be preferred when you have a model +behind a form. Additionally there are some useful methods in [[\yii\helpers\Html]] that are typically used for adding +buttons and help text. + +First step creating a form is to create a model. It can be either Active Record or regular Model. Let's use regular +login model as an example: + +```php +use yii\base\Model; + +class LoginForm extends Model +{ + public $username; + public $password; + + /** + * @return array the validation rules. + */ + public function rules() + { + return [ + // username and password are both required + ['username, password', 'required'], + // password is validated by validatePassword() + ['password', 'validatePassword'], + ]; + } + + /** + * Validates the password. + * This method serves as the inline validation for password. + */ + public function validatePassword() + { + $user = User::findByUsername($this->username); + if (!$user || !$user->validatePassword($this->password)) { + $this->addError('password', 'Incorrect username or password.'); + } + } + + /** + * Logs in a user using the provided username and password. + * @return boolean whether the user is logged in successfully + */ + public function login() + { + if ($this->validate()) { + $user = User::findByUsername($this->username); + return true; + } else { + return false; + } + } +} +``` + +In controller we're passing model to view where Active Form is used: + +```php +use yii\helpers\Html; +use yii\widgets\ActiveForm; + + 'login-form', + 'options' => ['class' => 'form-horizontal'], +]) ?> + field($model, 'username') ?> + field($model, 'password')->passwordInput() ?> + +
+
+ 'btn btn-primary']) ?> +
+
+ +``` + +In the code above `ActiveForm::begin()` not only creates form instance but marks the beginning of the form. All the content +that is located between `ActiveForm::begin()` and `ActiveForm::end()` will be wrapped with appropriate `
` tag. +Same as with any other widget you can specify some options passing an array to `begin` method. In our case we're adding +extra CSS class and specifying ID that will be used in the tag. + +In order to insert a form field along with its label all necessary validation JavaScript we're calling `field` method +and it gives back `\yii\widgets\ActiveField`. It it's echoed directly it creates a regular input. In case you want to +customize it you can add a chain of additional methods: + +```php +field($model, 'password')->passwordInput() ?> + +// or + +field($model, 'username')->textInput()->hint('Please enter your name')->label('Name') ?> +``` From b355019278dc7efa8ba0a06f880eee30a8096e48 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 00:12:24 +0400 Subject: [PATCH 016/109] some docs on Authentication --- docs/guide/authentication.md | 72 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/docs/guide/authentication.md b/docs/guide/authentication.md index e69de29..216b4c6 100644 --- a/docs/guide/authentication.md +++ b/docs/guide/authentication.md @@ -0,0 +1,72 @@ +Authentication +============== + +Authentication is basically what happens when one is trying to sign in. Typically login and passwords are read from +the form and then application checks if there's such user with such password. + +In Yii all this is done semi-automatically and what's left to developer is to implement [[\yii\web\IdentityInterface]]. +Typically it is being implemented in `User` model. You can find a full featured example in +[advanced application template](installation.md). Below only interface methods are listed: + +```php +class User extends ActiveRecord implements IdentityInterface +{ + // ... + + /** + * Finds an identity by the given ID. + * + * @param string|integer $id the ID to be looked for + * @return IdentityInterface|null the identity object that matches the given ID. + */ + public static function findIdentity($id) + { + return static::find($id); + } + + /** + * @return int|string current user ID + */ + public function getId() + { + return $this->id; + } + + /** + * @return string current user auth key + */ + public function getAuthKey() + { + return $this->auth_key; + } + + /** + * @param string $authKey + * @return boolean if auth key is valid for current user + */ + public function validateAuthKey($authKey) + { + return $this->getAuthKey() === $authKey; + } +} +``` + +First two methods are simple. `findIdentity` given ID returns model instance while `getId` returns ID itself. +`getAuthKey` and `validateAuthKey` are used to provide extra security to the "remember me" cookie. +`getAuthKey` should return a string that is unique for each user. A good idea is to save this value when user is +created using `Security::generateRandomKey()`: + +```php +public function beforeSave($insert) +{ + if (parent::beforeSave($insert)) { + if ($this->isNewRecord) { + $this->auth_key = Security::generateRandomKey(); + } + return true; + } + return false; +} +``` + +`validateAuthKey` just compares `$authKey` passed as parameter (got from cookie) with the value got from database. From 4c8a11793d45062b02d948bf2baf4d3292b2b963 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 01:17:58 +0400 Subject: [PATCH 017/109] started authorization docs --- docs/guide/authorization.md | 124 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/docs/guide/authorization.md b/docs/guide/authorization.md index e69de29..47b9409 100644 --- a/docs/guide/authorization.md +++ b/docs/guide/authorization.md @@ -0,0 +1,124 @@ +Authorization +============= + +Authorization is the process of verifying that user has enough permissions to do something. Yii provides several methods +of controlling it. + +Access control basics +--------------------- + +Basic acces control is very simple to implement using [[\yii\web\AccessControl]]: + +```php +class SiteController extends Controller +{ + public function behaviors() + { + return [ + 'access' => [ + 'class' => \yii\web\AccessControl::className(), + 'only' => ['login', 'logout', 'signup'], + 'rules' => [ + [ + 'actions' => ['login', 'signup'], + 'allow' => true, + 'roles' => ['?'], + ], + [ + 'actions' => ['logout'], + 'allow' => true, + 'roles' => ['@'], + ], + ], + ], + ]; + } + // ... +``` + +In the code above we're attaching access control behavior to a controller. Since there's `only` option specified, it +will be applied to 'login', 'logout' and 'signup' actions only. A set of rules that are basically options for +[[\yii\web\AccessRule]] reads as follows: + +- Allow all guest (not yet authenticated) users to access 'login' and 'signup' actions. +- Allow authenticated users to access 'logout' action. + +Rules are checked one by one from top to bottom. If rule matches, action takes place immediately. If not, next rule is +checked. If no rules matched access is denied. + +[[\yii\web\AccessRule]] is quite flexible and allows additionally to what was demonstrated checking IPs and request method +(i.e. POST, GET). If it's not enough you can specify your own check via anonymous function: + +```php +class SiteController extends Controller +{ + public function behaviors() + { + return [ + 'access' => [ + 'class' => \yii\web\AccessControl::className(), + 'only' => ['special'], + 'rules' => [ + [ + 'actions' => ['special'], + 'allow' => true, + 'matchCallback' => function ($rule, $action) { + return date('d-m') === '31-10'; + } + ], +``` + +Sometimes you want a custom action to be taken when access is denied. In this case you can specify `denyCallback`. + +Role based access control (RBAC) +-------------------------------- + +Role based access control is very flexible approach to controlling access that is a perfect match for complex systems +where permissions are customizable. + +In order to start using it some extra steps are required. First of all we need to configure `authManager` application +component: + +```php + +``` + +Then create permissions hierarchy. + +Specify roles from RBAC in controller's access control configuration or call [[User::checkAccess()]] where appropriate. + +### How it works + +TBD: write about how it works with pictures :) + +### Avoiding too much RBAC + +In order to keep auth hierarchy simple and efficient you should avoid creating and using too much nodes. Most of the time +simple checks could be used instead. For example such code that uses RBAC: + +```php +public function editArticle($id) +{ + $article = Article::find($id); + if (!$article) { + throw new HttpException(404); + } + if (!\Yii::$app->user->checkAccess('edit_article', ['article' => $article])) { + throw new HttpException(403); + } + // ... +} +``` + +can be replaced with simpler code that doesn't use RBAC: + +```php +public function editArticle($id) +{ + $article = Article::find(['id' => $id, 'author_id' => \Yii::$app->user->id]); + if (!$article) { + throw new HttpException(404); + } + // ... +} +``` From d13aaa1e85457c2ee91d7b244b426eb0423e22a7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 01:25:00 +0400 Subject: [PATCH 018/109] added composer basic docs --- docs/guide/composer.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ docs/guide/index.md | 1 + 2 files changed, 51 insertions(+) create mode 100644 docs/guide/composer.md diff --git a/docs/guide/composer.md b/docs/guide/composer.md new file mode 100644 index 0000000..7f72f9c --- /dev/null +++ b/docs/guide/composer.md @@ -0,0 +1,50 @@ +Composer +======== + +Yii2 uses Composer as its package manager. It is a PHP utility that allows you to automatically install libraries and +extensions keeping them up to date and handling dependencies. + +Installing Composer +------------------- + +Check the official guide for [linux](http://getcomposer.org/doc/00-intro.md#installation-nix) or +[Windows](http://getcomposer.org/doc/00-intro.md#installation-windows). + +Adding more packages to your project +------------------------------------ + +After [installing an application](installing.md) you will find `composer.json` in the root directory of your project. +This file lists packages that your application uses. The part we're interested in is `require` section. + +``` +{ + "require": { + "Michelf/php-markdown": ">=1.3", + "ezyang/htmlpurifier": ">=4.5.0" + } +} +``` + +Here you can specify package name and version. Additionally to Yii extensions you may check +[packagist](http://packagist.org/) repository for general purpose PHP packages. + +After packages are specified you can type either + +``` +php composer.phar install +``` + +or + +``` +php composer.phar update +``` + +depending if you're doing it for the first time or not. Then, after some waiting, packages will be installed and ready +to use. You don't need anything to be configured additionally. + + +See also +-------- + +- [Official Composer documentation](http://getcomposer.org). \ No newline at end of file diff --git a/docs/guide/index.md b/docs/guide/index.md index 601323a..95a204f 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -77,4 +77,5 @@ More - [Performance Tuning](performance.md) - [Managing assets](assets.md) - [Testing](testing.md) +- [Composer](composer.md) - [Upgrading from 1.1 to 2.0](upgrade-from-v1.md) From 986a8b0ca49d9f98b75a21bba975691207e5a570 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 02:50:39 +0400 Subject: [PATCH 019/109] Fixes #1105: better validation for Gii model generator --- framework/yii/gii/generators/model/Generator.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/framework/yii/gii/generators/model/Generator.php b/framework/yii/gii/generators/model/Generator.php index a7f1aa7..99f2d38 100644 --- a/framework/yii/gii/generators/model/Generator.php +++ b/framework/yii/gii/generators/model/Generator.php @@ -61,7 +61,7 @@ class Generator extends \yii\gii\Generator ['db', 'validateDb'], ['ns', 'validateNamespace'], ['tableName', 'validateTableName'], - ['modelClass', 'validateModelClass'], + ['modelClass', 'validateModelClass', 'skipOnEmpty' => false], ['baseClass', 'validateClass', 'params' => ['extends' => ActiveRecord::className()]], ['generateRelations, generateLabelsFromComments', 'boolean'], ]); @@ -93,14 +93,14 @@ class Generator extends \yii\gii\Generator 'db' => 'This is the ID of the DB application component.', 'tableName' => 'This is the name of the DB table that the new ActiveRecord class is associated with, e.g. tbl_post. The table name may consist of the DB schema part if needed, e.g. public.tbl_post. - The table name may contain an asterisk to match multiple table names, e.g. tbl_* + The table name may end with asterisk to match multiple table names, e.g. tbl_* will match tables who name starts with tbl_. In this case, multiple ActiveRecord classes will be generated, one for each matching table name; and the class names will be generated from the matching characters. For example, table tbl_post will generate Post class.', 'modelClass' => 'This is the name of the ActiveRecord class to be generated. The class name should not contain the namespace part as it is specified in "Namespace". You do not need to specify the class name - if "Table Name" contains an asterisk at the end, in which case multiple ActiveRecord classes will be generated.', + if "Table Name" ends with asterisk, in which case multiple ActiveRecord classes will be generated.', 'baseClass' => 'This is the base class of the new ActiveRecord class. It should be a fully qualified namespaced class name.', 'generateRelations' => 'This indicates whether the generator should generate relations based on foreign key constraints it detects in the database. Note that if your database contains too many tables, @@ -434,8 +434,8 @@ class Generator extends \yii\gii\Generator if ($this->isReservedKeyword($this->modelClass)) { $this->addError('modelClass', 'Class name cannot be a reserved PHP keyword.'); } - if (strpos($this->tableName, '*') === false && $this->modelClass == '') { - $this->addError('modelClass', 'Model Class cannot be blank.'); + if (substr($this->tableName, -1) !== '*' && $this->modelClass == '') { + $this->addError('modelClass', 'Model Class cannot be blank if table name does not end with asterisk.'); } } @@ -444,8 +444,8 @@ class Generator extends \yii\gii\Generator */ public function validateTableName() { - if (($pos = strpos($this->tableName, '*')) !== false && strpos($this->tableName, '*', $pos + 1) !== false) { - $this->addError('tableName', 'At most one asterisk is allowed.'); + if (($pos = strpos($this->tableName, '*')) !== false && substr($this->tableName, -1) !== '*') { + $this->addError('tableName', 'Asterisk is only allowed as the last character.'); return; } $tables = $this->getTableNames(); From 3dbfd3ea0eeb3be7485074003bb2a1f7c4ce7d08 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 03:13:40 +0400 Subject: [PATCH 020/109] Gii: renamed action "new" to "create" --- framework/yii/gii/CodeFile.php | 6 +++--- framework/yii/gii/Generator.php | 2 +- framework/yii/gii/views/default/view/files.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/framework/yii/gii/CodeFile.php b/framework/yii/gii/CodeFile.php index 2676c38..a5196d9 100644 --- a/framework/yii/gii/CodeFile.php +++ b/framework/yii/gii/CodeFile.php @@ -28,7 +28,7 @@ class CodeFile extends Object /** * The code file is new. */ - const OP_NEW = 'new'; + const OP_CREATE = 'create'; /** * The code file already exists, and the new one may need to overwrite it. */ @@ -68,7 +68,7 @@ class CodeFile extends Object if (is_file($path)) { $this->operation = file_get_contents($path) === $content ? self::OP_SKIP : self::OP_OVERWRITE; } else { - $this->operation = self::OP_NEW; + $this->operation = self::OP_CREATE; } } @@ -79,7 +79,7 @@ class CodeFile extends Object public function save() { $module = Yii::$app->controller->module; - if ($this->operation === self::OP_NEW) { + if ($this->operation === self::OP_CREATE) { $dir = dirname($this->path); if (!is_dir($dir)) { $mask = @umask(0); diff --git a/framework/yii/gii/Generator.php b/framework/yii/gii/Generator.php index 83cdf6e..6f63628 100644 --- a/framework/yii/gii/Generator.php +++ b/framework/yii/gii/Generator.php @@ -250,7 +250,7 @@ abstract class Generator extends Model $hasError = true; $lines[] = "generating $relativePath\n$error"; } else { - $lines[] = $file->operation === CodeFile::OP_NEW ? " generated $relativePath" : " overwrote $relativePath"; + $lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath"; } } else { $lines[] = " skipped $relativePath"; diff --git a/framework/yii/gii/views/default/view/files.php b/framework/yii/gii/views/default/view/files.php index af61a02..299f12b 100644 --- a/framework/yii/gii/views/default/view/files.php +++ b/framework/yii/gii/views/default/view/files.php @@ -53,7 +53,7 @@ use yii\gii\CodeFile; if ($file->operation === CodeFile::OP_SKIP) { echo ' '; } else { - echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_NEW)); + echo Html::checkBox("answers[{$file->id}]", isset($answers) ? isset($answers[$file->id]) : ($file->operation === CodeFile::OP_CREATE)); } ?> From 91b61692c5b3f083ec27ccfaa6c4c9d3da7e9d28 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 03:16:34 +0400 Subject: [PATCH 021/109] Better alias paths for advanced application --- apps/advanced/common/config/params.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/advanced/common/config/params.php b/apps/advanced/common/config/params.php index 7dab548..5212f84 100644 --- a/apps/advanced/common/config/params.php +++ b/apps/advanced/common/config/params.php @@ -1,8 +1,8 @@ 'admin@example.com', From 1c1cd863cadf87dde34d9b0ba5204aa85bd8c691 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 03:30:40 +0400 Subject: [PATCH 022/109] Fixes #1107: Gii CRUD generator now validates same named model and search model class names --- framework/yii/gii/generators/crud/Generator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/yii/gii/generators/crud/Generator.php b/framework/yii/gii/generators/crud/Generator.php index cb4bce6..a38cf5e 100644 --- a/framework/yii/gii/generators/crud/Generator.php +++ b/framework/yii/gii/generators/crud/Generator.php @@ -44,6 +44,7 @@ class Generator extends \yii\gii\Generator return array_merge(parent::rules(), [ ['moduleID, controllerClass, modelClass, searchModelClass, baseControllerClass', 'filter', 'filter' => 'trim'], ['modelClass, searchModelClass, controllerClass, baseControllerClass, indexWidgetType', 'required'], + ['searchModelClass', 'compare', 'compareAttribute' => 'modelClass', 'operator' => '!==', 'message' => 'Search Model Class must not be equal to Model Class.'], ['modelClass, controllerClass, baseControllerClass, searchModelClass', 'match', 'pattern' => '/^[\w\\\\]*$/', 'message' => 'Only word characters and backslashes are allowed.'], ['modelClass', 'validateClass', 'params' => ['extends' => ActiveRecord::className()]], ['baseControllerClass', 'validateClass', 'params' => ['extends' => Controller::className()]], From eeed9c3ff20994079b24991dac5f2d22b663530b Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Thu, 31 Oct 2013 20:56:13 -0400 Subject: [PATCH 023/109] Fixes #998: Added support for generating canonical URL. --- framework/yii/web/Controller.php | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php index 9d22d01..6927893 100644 --- a/framework/yii/web/Controller.php +++ b/framework/yii/web/Controller.php @@ -24,6 +24,10 @@ class Controller extends \yii\base\Controller * CSRF validation is enabled only when both this property and [[Request::enableCsrfValidation]] are true. */ public $enableCsrfValidation = true; + /** + * @var array the parameters bound to the current action. This is mainly used by [[getCanonicalUrl()]]. + */ + public $actionParams = []; /** * Binds the parameters to the action. @@ -46,13 +50,14 @@ class Controller extends \yii\base\Controller $args = []; $missing = []; + $actionParams = []; foreach ($method->getParameters() as $param) { $name = $param->getName(); if (array_key_exists($name, $params)) { - $args[] = $params[$name]; + $args[] = $actionParams[$name] = $params[$name]; unset($params[$name]); } elseif ($param->isDefaultValueAvailable()) { - $args[] = $param->getDefaultValue(); + $args[] = $actionParams[$name] = $param->getDefaultValue(); } else { $missing[] = $name; } @@ -63,6 +68,8 @@ class Controller extends \yii\base\Controller 'params' => implode(', ', $missing), ])); } + + $this->actionParams = $actionParams; return $args; } @@ -113,6 +120,22 @@ class Controller extends \yii\base\Controller } /** + * Returns the canonical URL of the currently requested page. + * The canonical URL is constructed using [[route]] and [[actionParams]]. You may use the following code + * in the layout view to add a link tag about canonical URL: + * + * ~~~ + * $this->registerLinkTag(['rel' => 'canonical', 'href' => Yii::$app->controller->canonicalUrl]); + * ~~~ + * + * @return string + */ + public function getCanonicalUrl() + { + return Yii::$app->getUrlManager()->createAbsoluteUrl($this->getRoute(), $this->actionParams); + } + + /** * Redirects the browser to the specified URL. * This method is a shortcut to [[Response::redirect()]]. * From 1b497ad73be9f80e5fb122419ffe2c06875dfe56 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 1 Nov 2013 13:23:05 +0400 Subject: [PATCH 024/109] Fixes #1107: if basename of Gii CRUD model and search model are equal than alias is automatically used for search model --- framework/yii/gii/generators/crud/templates/controller.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/framework/yii/gii/generators/crud/templates/controller.php b/framework/yii/gii/generators/crud/templates/controller.php index d1921de..a863b30 100644 --- a/framework/yii/gii/generators/crud/templates/controller.php +++ b/framework/yii/gii/generators/crud/templates/controller.php @@ -12,6 +12,9 @@ use yii\helpers\StringHelper; $controllerClass = StringHelper::basename($generator->controllerClass); $modelClass = StringHelper::basename($generator->modelClass); $searchModelClass = StringHelper::basename($generator->searchModelClass); +if ($modelClass === $searchModelClass) { + $searchModelAlias = $searchModelClass.'Search'; +} $pks = $generator->getTableSchema()->primaryKey; $urlParams = $generator->generateUrlParams(); @@ -24,7 +27,7 @@ echo "controllerClass, '\\')) ?>; use modelClass, '\\') ?>; -use searchModelClass, '\\') ?>; +use searchModelClass, '\\') ?> as ; use yii\data\ActiveDataProvider; use baseControllerClass, '\\') ?>; use yii\web\HttpException; @@ -53,7 +56,7 @@ class extends bas */ public function actionIndex() { - $searchModel = new ; + $searchModel = new ; $dataProvider = $searchModel->search($_GET); return $this->render('index', [ From bd8217014204ed37e31f2d1b3985082652979fd4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Nov 2013 10:03:13 -0400 Subject: [PATCH 025/109] Fixes #1096: pgsql: sequence name not matched --- framework/yii/db/pgsql/Schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index c4da801..20c6fb6 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/framework/yii/db/pgsql/Schema.php @@ -281,7 +281,7 @@ SQL; $table->columns[$column->name] = $column; if ($column->isPrimaryKey === true) { $table->primaryKey[] = $column->name; - if ($table->sequenceName === null && preg_match("/nextval\('\w+'(::regclass)?\)/", $column->defaultValue) === 1) { + if ($table->sequenceName === null && preg_match("/nextval\\('\"?\\w+\"?'(::regclass)?\\)/", $column->defaultValue) === 1) { $table->sequenceName = preg_replace(['/nextval/', '/::/', '/regclass/', '/\'\)/', '/\(\'/'], '', $column->defaultValue); } } From 1bee84746c07232731cbaaf1e2735cca2a5955a3 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Nov 2013 10:21:54 -0400 Subject: [PATCH 026/109] Fixes #1060: added code comment --- framework/yii/gii/generators/crud/templates/views/index.php | 2 +- framework/yii/widgets/ActiveField.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/yii/gii/generators/crud/templates/views/index.php b/framework/yii/gii/generators/crud/templates/views/index.php index d3cebcc..8543fad 100644 --- a/framework/yii/gii/generators/crud/templates/views/index.php +++ b/framework/yii/gii/generators/crud/templates/views/index.php @@ -15,7 +15,7 @@ echo " use yii\helpers\Html; -use indexWidgetType === 'grid' ? "yii\grid\GridView" : "yii\widgets\ListView" ?>; +use indexWidgetType === 'grid' ? "yii\\grid\\GridView" : "yii\\widgets\\ListView" ?>; /** * @var yii\base\View $this diff --git a/framework/yii/widgets/ActiveField.php b/framework/yii/widgets/ActiveField.php index fc30af5..dc97cbd 100644 --- a/framework/yii/widgets/ActiveField.php +++ b/framework/yii/widgets/ActiveField.php @@ -324,6 +324,7 @@ class ActiveField extends Component */ public function fileInput($options = []) { + // https://github.com/yiisoft/yii2/pull/795 if ($this->inputOptions !== ['class' => 'form-control']) { $options = array_merge($this->inputOptions, $options); } From e5a1244e0f630ac56a3d356b5a22cf715f2b45b1 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 16:46:41 +0100 Subject: [PATCH 027/109] moved unit test from cubrid to general should be tested for all dbms not only cubrid even if the problem may not exist in all of them it is good to verify that. --- tests/unit/framework/db/ActiveRecordTest.php | 28 ++++++++++++++++++++++ .../framework/db/cubrid/CubridActiveRecordTest.php | 28 ---------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 1d6005e..9e68f5e 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -426,4 +426,32 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertEquals(0, $record->var3); $this->assertEquals('', $record->stringcol); } + + /** + * Some PDO implementations(e.g. cubrid) do not support boolean values. + * Make sure this does not affect AR layer. + */ + public function testBooleanAttribute() + { + $customer = new Customer(); + $customer->name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(1, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(0, $customer->status); + + $customers = Customer::find()->where(['status' => true])->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->where(['status' => false])->all(); + $this->assertEquals(1, count($customers)); + } } diff --git a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php index 9fb9915..3949ba2 100644 --- a/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php +++ b/tests/unit/framework/db/cubrid/CubridActiveRecordTest.php @@ -11,32 +11,4 @@ use yiiunit\framework\db\ActiveRecordTest; class CubridActiveRecordTest extends ActiveRecordTest { public $driverName = 'cubrid'; - - /** - * cubrid PDO does not support boolean values. - * Make sure this does not affect AR layer. - */ - public function testBooleanAttribute() - { - $customer = new Customer(); - $customer->name = 'boolean customer'; - $customer->email = 'mail@example.com'; - $customer->status = true; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(1, $customer->status); - - $customer->status = false; - $customer->save(false); - - $customer->refresh(); - $this->assertEquals(0, $customer->status); - - $customers = Customer::find()->where(['status' => true])->all(); - $this->assertEquals(2, count($customers)); - - $customers = Customer::find()->where(['status' => false])->all(); - $this->assertEquals(1, count($customers)); - } } From 7c630a6246d70ac0d9af53e1cfcdf0100215cd24 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 17:02:31 +0100 Subject: [PATCH 028/109] fixed problem with Postgres PDO and Boolean values See the following resources for details: yiisoft/yii#779 https://bugs.php.net/bug.php?id=33876 http://www.yiiframework.com/forum/index.php/topic/32334-boolean-type-with-postgresql/page__view__findpost__p__155647 --- framework/yii/db/pgsql/Schema.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index 20c6fb6..bb38605 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/framework/yii/db/pgsql/Schema.php @@ -318,4 +318,24 @@ SQL; $column->phpType = $this->getColumnPhpType($column); return $column; } + + /** + * Determines the PDO type for the given PHP data value. + * @param mixed $data the data whose PDO type is to be determined + * @return integer the PDO type + * @see http://www.php.net/manual/en/pdo.constants.php + */ + public function getPdoType($data) + { + static $typeMap = [ + // php type => PDO type + 'boolean' => \PDO::PARAM_INT, // Cast boolean to integer values to work around problems with PDO casting false to string '' https://bugs.php.net/bug.php?id=33876 + 'integer' => \PDO::PARAM_INT, + 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, + 'NULL' => \PDO::PARAM_NULL, + ]; + $type = gettype($data); + return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; + } } From f153ce443ab91e51c8379f9d463c733d2ad5a02a Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 17:20:19 +0100 Subject: [PATCH 029/109] reverted non working fix for #1115 --- framework/yii/db/pgsql/Schema.php | 20 -------------------- .../db/pgsql/PostgreSQLActiveRecordTest.php | 5 +++++ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php index bb38605..20c6fb6 100644 --- a/framework/yii/db/pgsql/Schema.php +++ b/framework/yii/db/pgsql/Schema.php @@ -318,24 +318,4 @@ SQL; $column->phpType = $this->getColumnPhpType($column); return $column; } - - /** - * Determines the PDO type for the given PHP data value. - * @param mixed $data the data whose PDO type is to be determined - * @return integer the PDO type - * @see http://www.php.net/manual/en/pdo.constants.php - */ - public function getPdoType($data) - { - static $typeMap = [ - // php type => PDO type - 'boolean' => \PDO::PARAM_INT, // Cast boolean to integer values to work around problems with PDO casting false to string '' https://bugs.php.net/bug.php?id=33876 - 'integer' => \PDO::PARAM_INT, - 'string' => \PDO::PARAM_STR, - 'resource' => \PDO::PARAM_LOB, - 'NULL' => \PDO::PARAM_NULL, - ]; - $type = gettype($data); - return isset($typeMap[$type]) ? $typeMap[$type] : \PDO::PARAM_STR; - } } diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php index 1fffad7..4146e8c 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php @@ -11,4 +11,9 @@ use yiiunit\framework\db\ActiveRecordTest; class PostgreSQLActiveRecordTest extends ActiveRecordTest { protected $driverName = 'pgsql'; + + public function testBooleanAttribute() + { + $this->markTestSkipped('Storing boolean values does not work in PostgreSQL right now. See https://github.com/yiisoft/yii2/issues/1115 for details.'); + } } From 937a55f4fa11a325ac64c9673a8393fc482e56a5 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 17:39:49 +0100 Subject: [PATCH 030/109] fixed unit test for sqlite --- .../framework/db/sqlite/SqliteActiveRecordTest.php | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php index a689e5d..5afcff2 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -1,6 +1,7 @@ name = 'boolean customer'; + $customer->email = 'mail@example.com'; + $customer->status = true; + $customer->save(false); + + $customer->refresh(); + $this->assertEquals(1, $customer->status); + + $customer->status = false; + $customer->save(false); + + $customer->refresh(); + // sqlite will return empty string here but it would still + // evaluate to false or null so we accept it + $this->assertTrue(0 == $customer->status); + + $customers = Customer::find()->where(['status' => true])->all(); + $this->assertEquals(2, count($customers)); + + $customers = Customer::find()->where(['status' => false])->all(); + $this->assertEquals(1, count($customers)); + } } From ee1689da036f519930fa9567fee61650e969f195 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 17:57:28 +0100 Subject: [PATCH 031/109] some more on active record unit tests and sqlite sqlite does not seem to allow using boolean values in select query --- tests/unit/framework/db/ActiveRecordTest.php | 19 +++++++++++++++++++ .../framework/db/sqlite/SqliteActiveRecordTest.php | 11 ++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index 9e68f5e..a86c084 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -427,6 +427,25 @@ class ActiveRecordTest extends DatabaseTestCase $this->assertEquals('', $record->stringcol); } + public function testStoreEmpty() + { + $record = new NullValues(); + $record->id = 1; + + // this is to simulate empty html form submission + $record->var1 = ''; + $record->var2 = ''; + $record->var3 = ''; + $record->stringcol = ''; + + $record->save(false); + $this->assertTrue($record->refresh()); + + // https://github.com/yiisoft/yii2/commit/34945b0b69011bc7cab684c7f7095d837892a0d4#commitcomment-4458225 + $this->assertTrue($record->var1 === $record->var2); + $this->assertTrue($record->var2 === $record->var3); + } + /** * Some PDO implementations(e.g. cubrid) do not support boolean values. * Make sure this does not affect AR layer. diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php index 5afcff2..659908e 100644 --- a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -35,10 +35,11 @@ class SqliteActiveRecordTest extends ActiveRecordTest // evaluate to false or null so we accept it $this->assertTrue(0 == $customer->status); - $customers = Customer::find()->where(['status' => true])->all(); - $this->assertEquals(2, count($customers)); - - $customers = Customer::find()->where(['status' => false])->all(); - $this->assertEquals(1, count($customers)); + // select with boolean values does not seem to work in sqlite +// $customers = Customer::find()->where(['status' => true])->all(); +// $this->assertEquals(2, count($customers)); +// +// $customers = Customer::find()->where(['status' => false])->all(); +// $this->assertEquals(1, count($customers)); } } From fedc38fdb61554f161aaf46ed41f718f7e72f4b7 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Nov 2013 13:08:01 -0400 Subject: [PATCH 032/109] Fixes #1116. --- framework/yii/gii/assets/gii.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/yii/gii/assets/gii.js b/framework/yii/gii/assets/gii.js index b581d3b..a95221e 100644 --- a/framework/yii/gii/assets/gii.js +++ b/framework/yii/gii/assets/gii.js @@ -14,7 +14,7 @@ yii.gii = (function ($) { }; var initStickyInputs = function () { - $('.sticky:not(.error) input[type="text"],select,textarea').each(function () { + $('.sticky:not(.error)').find('input[type="text"],select,textarea').each(function () { var value; if (this.tagName === 'SELECT') { value = this.options[this.selectedIndex].text; From c8c377e698595fa7af78c85c116ba32e6eb7f584 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 1 Nov 2013 18:10:57 +0100 Subject: [PATCH 033/109] skip test on postgres --- tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php index 4146e8c..d41a837 100644 --- a/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php +++ b/tests/unit/framework/db/pgsql/PostgreSQLActiveRecordTest.php @@ -16,4 +16,9 @@ class PostgreSQLActiveRecordTest extends ActiveRecordTest { $this->markTestSkipped('Storing boolean values does not work in PostgreSQL right now. See https://github.com/yiisoft/yii2/issues/1115 for details.'); } + + public function testStoreEmpty() + { + // as this test attempts to store data with invalid type it is okay for postgres to fail, skipping silently. + } } From 4b42d78f4ef38666137dd883c1f18fa44c264c32 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Fri, 1 Nov 2013 21:43:33 -0400 Subject: [PATCH 034/109] Fixes #1117: added support to map a single view directory to multiple themed view directories. --- framework/yii/base/Theme.php | 51 +++++++++++++++++++++++++++++++++----------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/framework/yii/base/Theme.php b/framework/yii/base/Theme.php index ff6780c..b864412 100644 --- a/framework/yii/base/Theme.php +++ b/framework/yii/base/Theme.php @@ -13,17 +13,38 @@ use yii\helpers\FileHelper; /** * Theme represents an application theme. * - * A theme is directory consisting of view and layout files which are meant to replace their - * non-themed counterparts. + * When [[View]] renders a view file, it will check the [[Application::theme|active theme]] + * to see if there is a themed version of the view file exists. If so, the themed version will be rendered instead. * - * Theme uses [[pathMap]] to achieve the file replacement. A view or layout file will be replaced - * with its themed version if part of its path matches one of the keys in [[pathMap]]. - * Then the matched part will be replaced with the corresponding array value. + * A theme is directory consisting of view files which are meant to replace their non-themed counterparts. + * + * Theme uses [[pathMap]] to achieve the view file replacement: + * + * 1. It first looks for a key in [[pathMap]] that is a substring of the given view file path; + * 2. If such a key exists, the corresponding value will be used to replace the corresponding part + * in the view file path; + * 3. It will then check if the updated view file exists or not. If so, that file will be used + * to replace the original view file. + * 4. If Step 2 or 3 fails, the original view file will be used. * * For example, if [[pathMap]] is `['/web/views' => '/web/themes/basic']`, * then the themed version for a view file `/web/views/site/index.php` will be * `/web/themes/basic/site/index.php`. * + * It is possible to map a single path to multiple paths. For example, + * + * ~~~ + * 'pathMap' => [ + * '/web/views' => [ + * '/web/themes/christmas', + * '/web/themes/basic', + * ], + * ] + * ~~~ + * + * In this case, the themed version could be either `/web/themes/christmas/site/index.php` or + * `/web/themes/basic/site/index.php`. The former has precedence over the latter if both files exist. + * * To use a theme, you should configure the [[View::theme|theme]] property of the "view" application * component like the following: * @@ -75,16 +96,18 @@ class Theme extends Component if (empty($this->pathMap)) { if ($this->basePath !== null) { $this->basePath = Yii::getAlias($this->basePath); - $this->pathMap = [Yii::$app->getBasePath() => $this->basePath]; + $this->pathMap = [Yii::$app->getBasePath() => [$this->basePath]]; } else { throw new InvalidConfigException('The "basePath" property must be set.'); } } $paths = []; - foreach ($this->pathMap as $from => $to) { + foreach ($this->pathMap as $from => $tos) { $from = FileHelper::normalizePath(Yii::getAlias($from)); - $to = FileHelper::normalizePath(Yii::getAlias($to)); - $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; + foreach ((array)$tos as $to) { + $to = FileHelper::normalizePath(Yii::getAlias($to)); + $paths[$from . DIRECTORY_SEPARATOR][] = $to . DIRECTORY_SEPARATOR; + } } $this->pathMap = $paths; if ($this->baseUrl === null) { @@ -103,12 +126,14 @@ class Theme extends Component public function applyTo($path) { $path = FileHelper::normalizePath($path); - foreach ($this->pathMap as $from => $to) { + foreach ($this->pathMap as $from => $tos) { if (strpos($path, $from) === 0) { $n = strlen($from); - $file = $to . substr($path, $n); - if (is_file($file)) { - return $file; + foreach ($tos as $to) { + $file = $to . substr($path, $n); + if (is_file($file)) { + return $file; + } } } } From 64641cbd766366941c3da0b059efc42859d09fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B2=D0=B0=D0=BD=20=D0=91=D0=B0=D0=B3=D0=B0=D0=B5?= =?UTF-8?q?=D0=B2?= Date: Sat, 2 Nov 2013 10:42:14 +0500 Subject: [PATCH 035/109] Add batchInsert method to yii\db\Migration --- framework/yii/db/Migration.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/framework/yii/db/Migration.php b/framework/yii/db/Migration.php index 307b02a..37fdf3f 100644 --- a/framework/yii/db/Migration.php +++ b/framework/yii/db/Migration.php @@ -158,6 +158,21 @@ class Migration extends \yii\base\Component } /** + * Creates and executes an batch INSERT SQL statement. + * The method will properly escape the column names, and bind the values to be inserted. + * @param string $table the table that new rows will be inserted into. + * @param array $columns the column names. + * @param array $rows the rows to be batch inserted into the table + */ + public function batchInsert($table, $columns, $rows) + { + echo " > insert into $table ..."; + $time = microtime(true); + $this->db->createCommand()->batchInsert($table, $columns, $rows)->execute(); + echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n"; + } + + /** * Creates and executes an UPDATE SQL statement. * The method will properly escape the column names and bind the values to be updated. * @param string $table the table to be updated. From cc5fe76c9ee421fe75d4e7bd8976ab109deee4a1 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 2 Nov 2013 16:31:30 +0400 Subject: [PATCH 036/109] Added ability to get all GET, POST, PUT, DELETE or PATCH parameters to Request --- framework/yii/web/Request.php | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php index 610e907..a6a92fa 100644 --- a/framework/yii/web/Request.php +++ b/framework/yii/web/Request.php @@ -290,59 +290,74 @@ class Request extends \yii\base\Request /** * Returns the named GET parameter value. * If the GET parameter does not exist, the second parameter to this method will be returned. - * @param string $name the GET parameter name + * @param string $name the GET parameter name. If not specified, whole $_GET is returned. * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. * @return mixed the GET parameter value * @see getPost */ - public function get($name, $defaultValue = null) + public function get($name = null, $defaultValue = null) { + if ($name === null) { + return $_GET; + } return isset($_GET[$name]) ? $_GET[$name] : $defaultValue; } /** * Returns the named POST parameter value. * If the POST parameter does not exist, the second parameter to this method will be returned. - * @param string $name the POST parameter name + * @param string $name the POST parameter name. If not specified, whole $_POST is returned. * @param mixed $defaultValue the default parameter value if the POST parameter does not exist. * @return mixed the POST parameter value * @see getParam */ - public function getPost($name, $defaultValue = null) + public function getPost($name = null, $defaultValue = null) { + if ($name === null) { + return $_POST; + } return isset($_POST[$name]) ? $_POST[$name] : $defaultValue; } /** * Returns the named DELETE parameter value. - * @param string $name the DELETE parameter name + * @param string $name the DELETE parameter name. If not specified, an array of DELETE parameters is returned. * @param mixed $defaultValue the default parameter value if the DELETE parameter does not exist. * @return mixed the DELETE parameter value */ - public function getDelete($name, $defaultValue = null) + public function getDelete($name = null, $defaultValue = null) { + if ($name === null) { + return $this->getRestParams(); + } return $this->getIsDelete() ? $this->getRestParam($name, $defaultValue) : null; } /** * Returns the named PUT parameter value. - * @param string $name the PUT parameter name + * @param string $name the PUT parameter name. If not specified, an array of PUT parameters is returned. * @param mixed $defaultValue the default parameter value if the PUT parameter does not exist. * @return mixed the PUT parameter value */ - public function getPut($name, $defaultValue = null) + public function getPut($name = null, $defaultValue = null) { + if ($name === null) { + return $this->getRestParams(); + } return $this->getIsPut() ? $this->getRestParam($name, $defaultValue) : null; } /** * Returns the named PATCH parameter value. - * @param string $name the PATCH parameter name + * @param string $name the PATCH parameter name. If not specified, an array of PATCH parameters is returned. * @param mixed $defaultValue the default parameter value if the PATCH parameter does not exist. * @return mixed the PATCH parameter value */ - public function getPatch($name, $defaultValue = null) + public function getPatch($name = null, $defaultValue = null) { + if ($name === null) { + return $this->getRestParams(); + } return $this->getIsPatch() ? $this->getRestParam($name, $defaultValue) : null; } From 62148a2e33ea372198fb3b5b8e2604d25a2487fc Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 2 Nov 2013 16:39:24 +0400 Subject: [PATCH 037/109] Changed php-diff dependency to use code from master (they haven't tagged changes we need yet) --- framework/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/composer.json b/framework/composer.json index 419b4b7..05c4360 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -68,7 +68,7 @@ "yiisoft/yii2-composer": "*", "ext-mbstring": "*", "lib-pcre": "*", - "phpspec/php-diff": "1.0.*", + "phpspec/php-diff": "dev-master", "ezyang/htmlpurifier": "4.5.*" }, "autoload": { From 18fbd7510a1f5c01ece1079f7346dc957812888e Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Sat, 2 Nov 2013 16:12:28 +0300 Subject: [PATCH 038/109] Missing return statement --- framework/yii/db/ActiveRecord.php | 2 ++ framework/yii/db/mssql/Schema.php | 2 ++ framework/yii/i18n/GettextMoFile.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/framework/yii/db/ActiveRecord.php b/framework/yii/db/ActiveRecord.php index 79d5146..2c1689c 100644 --- a/framework/yii/db/ActiveRecord.php +++ b/framework/yii/db/ActiveRecord.php @@ -1266,6 +1266,8 @@ class ActiveRecord extends Model $relation = $this->$getter(); if ($relation instanceof ActiveRelation) { return $relation; + } else { + return null; } } catch (UnknownMethodException $e) { throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e); diff --git a/framework/yii/db/mssql/Schema.php b/framework/yii/db/mssql/Schema.php index 0bb5924..deb92f9 100644 --- a/framework/yii/db/mssql/Schema.php +++ b/framework/yii/db/mssql/Schema.php @@ -118,6 +118,8 @@ class Schema extends \yii\db\Schema if ($this->findColumns($table)) { $this->findForeignKeys($table); return $table; + } else { + return null; } } diff --git a/framework/yii/i18n/GettextMoFile.php b/framework/yii/i18n/GettextMoFile.php index a92293c..4a0a93c 100644 --- a/framework/yii/i18n/GettextMoFile.php +++ b/framework/yii/i18n/GettextMoFile.php @@ -203,6 +203,8 @@ class GettextMoFile extends GettextFile { if ($byteCount > 0) { return fread($fileHandle, $byteCount); + } else { + return null; } } From 5860599ef7fc6788203348579c5cb8ca133a7760 Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Sat, 2 Nov 2013 17:03:37 +0300 Subject: [PATCH 039/109] PhpDoc comment --- framework/yii/db/sqlite/QueryBuilder.php | 1 + framework/yii/i18n/GettextMoFile.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php index 2a6f345..4a5407f 100644 --- a/framework/yii/db/sqlite/QueryBuilder.php +++ b/framework/yii/db/sqlite/QueryBuilder.php @@ -80,6 +80,7 @@ class QueryBuilder extends \yii\db\QueryBuilder * @param boolean $check whether to turn on or off the integrity check. * @param string $schema the schema of the tables. Meaningless for SQLite. * @param string $table the table name. Meaningless for SQLite. + * @return string the SQL statement for checking integrity * @throws NotSupportedException this is not supported by SQLite */ public function checkIntegrity($check = true, $schema = '', $table = '') diff --git a/framework/yii/i18n/GettextMoFile.php b/framework/yii/i18n/GettextMoFile.php index 4a0a93c..b4a016d 100644 --- a/framework/yii/i18n/GettextMoFile.php +++ b/framework/yii/i18n/GettextMoFile.php @@ -54,6 +54,7 @@ class GettextMoFile extends GettextFile * @param string $context message context * @return array message translations. Array keys are source messages and array values are translated messages: * source message => translated message. + * @throws Exception if unable to read the MO file */ public function load($filePath, $context) { @@ -128,6 +129,7 @@ class GettextMoFile extends GettextFile * @param array $messages message translations. Array keys are source messages and array values are * translated messages: source message => translated message. Note if the message has a context, * the message ID must be prefixed with the context with chr(4) as the separator. + * @throws Exception if unable to save the MO file */ public function save($filePath, $messages) { From eee63f172114970a9fd59801ff8b8d074e9f47e2 Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Sat, 2 Nov 2013 17:53:48 +0300 Subject: [PATCH 040/109] Small typos --- docs/api/db/ActiveRecord.md | 2 +- docs/guide/authorization.md | 2 +- docs/guide/i18n.md | 2 +- docs/guide/query-builder.md | 2 +- docs/guide/validation.md | 2 +- docs/guide/view.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/db/ActiveRecord.md b/docs/api/db/ActiveRecord.md index 70f171b..ef050d0 100644 --- a/docs/api/db/ActiveRecord.md +++ b/docs/api/db/ActiveRecord.md @@ -1,6 +1,6 @@ ActiveRecord implements the [Active Record design pattern](http://en.wikipedia.org/wiki/Active_record). The idea is that an ActiveRecord object is associated with a row in a database table -so object properties are mapped to colums of the corresponding database row. +so object properties are mapped to columns of the corresponding database row. For example, a `Customer` object is associated with a row in the `tbl_customer` table. Instead of writing raw SQL statements to access the data in the table, you can call intuitive methods available in the corresponding ActiveRecord class diff --git a/docs/guide/authorization.md b/docs/guide/authorization.md index 47b9409..b49f1af 100644 --- a/docs/guide/authorization.md +++ b/docs/guide/authorization.md @@ -7,7 +7,7 @@ of controlling it. Access control basics --------------------- -Basic acces control is very simple to implement using [[\yii\web\AccessControl]]: +Basic access control is very simple to implement using [[\yii\web\AccessControl]]: ```php class SiteController extends Controller diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md index 33f7758..6524801 100644 --- a/docs/guide/i18n.md +++ b/docs/guide/i18n.md @@ -121,7 +121,7 @@ extension. After installing and enabling it you will be able to use extended syn that allows you to specify formatting style. Full reference is [available at ICU website](http://icu-project.org/apiref/icu4c/classMessageFormat.html) but since it's -a bit crypric we have our own reference below. +a bit cryptic we have our own reference below. ### Numbers diff --git a/docs/guide/query-builder.md b/docs/guide/query-builder.md index ffc9871..7625c0b 100644 --- a/docs/guide/query-builder.md +++ b/docs/guide/query-builder.md @@ -45,7 +45,7 @@ $query->select(['tbl_user.name AS author', 'tbl_post.title as title']) // <-- sp ->leftJoin('tbl_post', 'tbl_post.user_id = tbl_user.id'); // <-- join with another table ``` -In the code above we've used `leftJoin` method to select from two related tables at the same time. Firsrt parameter +In the code above we've used `leftJoin` method to select from two related tables at the same time. First parameter specifies table name and the second is the join condition. Query builder has the following methods to join tables: - `innerJoin` diff --git a/docs/guide/validation.md b/docs/guide/validation.md index c8c8674..59242f5 100644 --- a/docs/guide/validation.md +++ b/docs/guide/validation.md @@ -8,7 +8,7 @@ Standard Yii validators ----------------------- Standard Yii validators could be specified using aliases instead of referring to class names. Here's the list of all -validators budled with Yii with their most useful properties: +validators bundled with Yii with their most useful properties: ### `boolean`: [[BooleanValidator]] diff --git a/docs/guide/view.md b/docs/guide/view.md index ee469e6..1069e89 100644 --- a/docs/guide/view.md +++ b/docs/guide/view.md @@ -38,7 +38,7 @@ Widgets Widgets are a self-contained building blocks for your views. A widget may contain advanced logic, typically takes some configuration and data and returns HTML. There is a good number of widgets bundled with Yii such as [active form](form.md), -breadcrumbs, menu or [wrappers around bootstrap component framework](boostrap-widgets.md). Additionally there are +breadcrumbs, menu or [wrappers around bootstrap component framework](bootstrap-widgets.md). Additionally there are extensions providing additional widgets such as official one for jQueryUI components. In order to use widget you need to do the following: From 5d17dd06d12a2702a6faeb0e8710c4c88b88a4de Mon Sep 17 00:00:00 2001 From: Alexander Mohorev Date: Sat, 2 Nov 2013 17:57:37 +0300 Subject: [PATCH 041/109] Specify the exact type of the exception. --- framework/yii/base/Module.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/yii/base/Module.php b/framework/yii/base/Module.php index 4bc5351..b3e28f6 100644 --- a/framework/yii/base/Module.php +++ b/framework/yii/base/Module.php @@ -241,7 +241,7 @@ abstract class Module extends Component * Sets the directory that contains the controller classes. * @param string $path the directory that contains the controller classes. * This can be either a directory name or a path alias. - * @throws Exception if the directory is invalid + * @throws InvalidParamException if the directory is invalid */ public function setControllerPath($path) { @@ -264,7 +264,7 @@ abstract class Module extends Component /** * Sets the directory that contains the view files. * @param string $path the root directory of view files. - * @throws Exception if the directory is invalid + * @throws InvalidParamException if the directory is invalid */ public function setViewPath($path) { @@ -287,7 +287,7 @@ abstract class Module extends Component /** * Sets the directory that contains the layout files. * @param string $path the root directory of layout files. - * @throws Exception if the directory is invalid + * @throws InvalidParamException if the directory is invalid */ public function setLayoutPath($path) { From 1d092d17555fe76d2656dd5557bfc7bd828d0f35 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 2 Nov 2013 13:06:04 -0400 Subject: [PATCH 042/109] Changed the exit status to normal. --- apps/advanced/init | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/advanced/init b/apps/advanced/init index c8f7f73..4015748 100755 --- a/apps/advanced/init +++ b/apps/advanced/init @@ -18,7 +18,7 @@ if (empty($params['env'])) { if (!ctype_digit($answer) || !in_array($answer, range(0, count($envs) - 1))) { echo "\n Quit initialization.\n"; - exit(1); + exit(0); } if (isset($envNames[$answer])) { @@ -42,7 +42,7 @@ if (empty($params['env'])) { $answer = trim(fgets(STDIN)); if (strncasecmp($answer, 'y', 1)) { echo "\n Quit initialization.\n"; - exit(1); + exit(0); } } From 13c3123ca8fbfe832c46ae3bf449f2b0eedc1cad Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 2 Nov 2013 14:16:38 -0400 Subject: [PATCH 043/109] moved mutex back from extensions. --- extensions/mutex/LICENSE.md | 32 --------- extensions/mutex/README.md | 42 ------------ extensions/mutex/composer.json | 27 -------- extensions/mutex/yii/mutex/DbMutex.php | 41 ------------ extensions/mutex/yii/mutex/FileMutex.php | 104 ------------------------------ extensions/mutex/yii/mutex/Mutex.php | 95 --------------------------- extensions/mutex/yii/mutex/MysqlMutex.php | 57 ---------------- framework/yii/mutex/DbMutex.php | 41 ++++++++++++ framework/yii/mutex/FileMutex.php | 104 ++++++++++++++++++++++++++++++ framework/yii/mutex/Mutex.php | 95 +++++++++++++++++++++++++++ framework/yii/mutex/MysqlMutex.php | 57 ++++++++++++++++ 11 files changed, 297 insertions(+), 398 deletions(-) delete mode 100644 extensions/mutex/LICENSE.md delete mode 100644 extensions/mutex/README.md delete mode 100644 extensions/mutex/composer.json delete mode 100644 extensions/mutex/yii/mutex/DbMutex.php delete mode 100644 extensions/mutex/yii/mutex/FileMutex.php delete mode 100644 extensions/mutex/yii/mutex/Mutex.php delete mode 100644 extensions/mutex/yii/mutex/MysqlMutex.php create mode 100644 framework/yii/mutex/DbMutex.php create mode 100644 framework/yii/mutex/FileMutex.php create mode 100644 framework/yii/mutex/Mutex.php create mode 100644 framework/yii/mutex/MysqlMutex.php diff --git a/extensions/mutex/LICENSE.md b/extensions/mutex/LICENSE.md deleted file mode 100644 index 0bb1a8d..0000000 --- a/extensions/mutex/LICENSE.md +++ /dev/null @@ -1,32 +0,0 @@ -The Yii framework is free software. It is released under the terms of -the following BSD License. - -Copyright © 2008-2013 by Yii Software LLC (http://www.yiisoft.com) -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in - the documentation and/or other materials provided with the - distribution. - * Neither the name of Yii Software LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGE. diff --git a/extensions/mutex/README.md b/extensions/mutex/README.md deleted file mode 100644 index 161ee8a..0000000 --- a/extensions/mutex/README.md +++ /dev/null @@ -1,42 +0,0 @@ -Yii 2.0 Public Preview - Mutex Extension -======================================== - -Thank you for choosing Yii - a high-performance component-based PHP framework. - -If you are looking for a production-ready PHP framework, please use -[Yii v1.1](https://github.com/yiisoft/yii). - -Yii 2.0 is still under heavy development. We may make significant changes -without prior notices. **Yii 2.0 is not ready for production use yet.** - -[![Build Status](https://secure.travis-ci.org/yiisoft/yii2.png)](http://travis-ci.org/yiisoft/yii2) - -This is the yii2-mutex extension. - - -Installation ------------- - -The prefered way to install this extension is through [composer](http://getcomposer.org/download/). - -Either run -``` -php composer.phar require yiisoft/yii2-mutex "*" -``` - -or add -``` -"yiisoft/yii2-mutex": "*" -``` -to the require section of your composer.json. - - -*Note: You might have to run `php composer.phar selfupdate`* - - -Usage & Documentation ---------------------- - -This component can be used to perform actions similar to the concept of [mutual exclusion](http://en.wikipedia.org/wiki/Mutual_exclusion). - -For concrete examples and advanced usage refer to the yii guide. diff --git a/extensions/mutex/composer.json b/extensions/mutex/composer.json deleted file mode 100644 index e0079ef..0000000 --- a/extensions/mutex/composer.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "yiisoft/yii2-mutex", - "description": "Mutual exclusion extension for the Yii framework", - "keywords": ["yii", "mutex"], - "type": "yii2-extension", - "license": "BSD-3-Clause", - "support": { - "issues": "https://github.com/yiisoft/yii2/issues?state=open", - "forum": "http://www.yiiframework.com/forum/", - "wiki": "http://www.yiiframework.com/wiki/", - "irc": "irc://irc.freenode.net/yii", - "source": "https://github.com/yiisoft/yii2" - }, - "authors": [ - { - "name": "resurtm", - "email": "resurtm@gmail.com" - } - ], - "minimum-stability": "dev", - "require": { - "yiisoft/yii2": "*" - }, - "autoload": { - "psr-0": { "yii\\mutex\\": "" } - } -} diff --git a/extensions/mutex/yii/mutex/DbMutex.php b/extensions/mutex/yii/mutex/DbMutex.php deleted file mode 100644 index 3699c36..0000000 --- a/extensions/mutex/yii/mutex/DbMutex.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @since 2.0 - */ -abstract class DbMutex extends Mutex -{ - /** - * @var Connection|string the DB connection object or the application component ID of the DB connection. - * After the Mutex object is created, if you want to change this property, you should only assign - * it with a DB connection object. - */ - public $db = 'db'; - - /** - * Initializes generic database table based mutex implementation. - * @throws InvalidConfigException if [[db]] is invalid. - */ - public function init() - { - parent::init(); - if (is_string($this->db)) { - $this->db = Yii::$app->getComponent($this->db); - } - if (!$this->db instanceof Connection) { - throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); - } - } -} diff --git a/extensions/mutex/yii/mutex/FileMutex.php b/extensions/mutex/yii/mutex/FileMutex.php deleted file mode 100644 index fd5cb00..0000000 --- a/extensions/mutex/yii/mutex/FileMutex.php +++ /dev/null @@ -1,104 +0,0 @@ - - * @since 2.0 - */ -class FileMutex extends Mutex -{ - /** - * @var string the directory to store mutex files. You may use path alias here. - * Defaults to the "mutex" subdirectory under the application runtime path. - */ - public $mutexPath = '@runtime/mutex'; - /** - * @var integer the permission to be set for newly created mutex files. - * This value will be used by PHP chmod() function. No umask will be applied. - * If not set, the permission will be determined by the current environment. - */ - public $fileMode; - /** - * @var integer the permission to be set for newly created directories. - * This value will be used by PHP chmod() function. No umask will be applied. - * Defaults to 0775, meaning the directory is read-writable by owner and group, - * but read-only for other users. - */ - public $dirMode = 0775; - /** - * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. - */ - private $_files = []; - - - /** - * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like - * operating systems. - * @throws InvalidConfigException - */ - public function init() - { - if (stripos(php_uname('s'), 'win') === 0) { - throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); - } - $this->mutexPath = Yii::getAlias($this->mutexPath); - if (!is_dir($this->mutexPath)) { - FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); - } - } - - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - */ - protected function acquireLock($name, $timeout = 0) - { - $fileName = $this->mutexPath . '/' . md5($name) . '.lock'; - $file = fopen($fileName, 'w+'); - if ($file === false) { - return false; - } - if ($this->fileMode !== null) { - @chmod($fileName, $this->fileMode); - } - $waitTime = 0; - while (!flock($file, LOCK_EX | LOCK_NB)) { - $waitTime++; - if ($waitTime > $timeout) { - fclose($file); - return false; - } - sleep(1); - } - $this->_files[$name] = $file; - return true; - } - - /** - * Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - */ - protected function releaseLock($name) - { - if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { - return false; - } else { - fclose($this->_files[$name]); - unset($this->_files[$name]); - return true; - } - } -} diff --git a/extensions/mutex/yii/mutex/Mutex.php b/extensions/mutex/yii/mutex/Mutex.php deleted file mode 100644 index 611e725..0000000 --- a/extensions/mutex/yii/mutex/Mutex.php +++ /dev/null @@ -1,95 +0,0 @@ - - * @since 2.0 - */ -abstract class Mutex extends Component -{ - /** - * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically - * before finishing script execution. Defaults to true. Setting this property to true means that all locks - * acquire in this process must be released in any case (regardless any kind of errors or exceptions). - */ - public $autoRelease = true; - /** - * @var string[] names of the locks acquired in the current PHP process. - */ - private $_locks = []; - - - /** - * Initializes the mutex component. - */ - public function init() - { - if ($this->autoRelease) { - $locks = &$this->_locks; - register_shutdown_function(function () use (&$locks) { - foreach ($locks as $lock) { - $this->release($lock); - } - }); - } - } - - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. Must be unique. - * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return - * false immediately in case lock was already acquired. - * @return boolean lock acquiring result. - */ - public function acquire($name, $timeout = 0) - { - if ($this->acquireLock($name, $timeout)) { - $this->_locks[] = $name; - return true; - } else { - return false; - } - } - - /** - * Release acquired lock. This method will return false in case named lock was not found. - * @param string $name of the lock to be released. This lock must be already created. - * @return boolean lock release result: false in case named lock was not found.. - */ - public function release($name) - { - if ($this->releaseLock($name)) { - $index = array_search($name, $this->_locks); - if ($index !== false) { - unset($this->_locks[$index]); - } - return true; - } else { - return false; - } - } - - /** - * This method should be extended by concrete mutex implementations. Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - */ - abstract protected function acquireLock($name, $timeout = 0); - - /** - * This method should be extended by concrete mutex implementations. Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - */ - abstract protected function releaseLock($name); -} diff --git a/extensions/mutex/yii/mutex/MysqlMutex.php b/extensions/mutex/yii/mutex/MysqlMutex.php deleted file mode 100644 index af05b9c..0000000 --- a/extensions/mutex/yii/mutex/MysqlMutex.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 2.0 - */ -class MysqlMutex extends Mutex -{ - /** - * Initializes MySQL specific mutex component implementation. - * @throws InvalidConfigException if [[db]] is not MySQL connection. - */ - public function init() - { - parent::init(); - if ($this->db->driverName !== 'mysql') { - throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); - } - } - - /** - * Acquires lock by given name. - * @param string $name of the lock to be acquired. - * @param integer $timeout to wait for lock to become released. - * @return boolean acquiring result. - * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock - */ - protected function acquireLock($name, $timeout = 0) - { - return (boolean)$this->db - ->createCommand('SELECT GET_LOCK(:name, :timeout)', [':name' => $name, ':timeout' => $timeout]) - ->queryScalar(); - } - - /** - * Releases lock by given name. - * @param string $name of the lock to be released. - * @return boolean release result. - * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock - */ - protected function releaseLock($name) - { - return (boolean)$this->db - ->createCommand('SELECT RELEASE_LOCK(:name)', [':name' => $name]) - ->queryScalar(); - } -} diff --git a/framework/yii/mutex/DbMutex.php b/framework/yii/mutex/DbMutex.php new file mode 100644 index 0000000..3699c36 --- /dev/null +++ b/framework/yii/mutex/DbMutex.php @@ -0,0 +1,41 @@ + + * @since 2.0 + */ +abstract class DbMutex extends Mutex +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the Mutex object is created, if you want to change this property, you should only assign + * it with a DB connection object. + */ + public $db = 'db'; + + /** + * Initializes generic database table based mutex implementation. + * @throws InvalidConfigException if [[db]] is invalid. + */ + public function init() + { + parent::init(); + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException('Mutex::db must be either a DB connection instance or the application component ID of a DB connection.'); + } + } +} diff --git a/framework/yii/mutex/FileMutex.php b/framework/yii/mutex/FileMutex.php new file mode 100644 index 0000000..fd5cb00 --- /dev/null +++ b/framework/yii/mutex/FileMutex.php @@ -0,0 +1,104 @@ + + * @since 2.0 + */ +class FileMutex extends Mutex +{ + /** + * @var string the directory to store mutex files. You may use path alias here. + * Defaults to the "mutex" subdirectory under the application runtime path. + */ + public $mutexPath = '@runtime/mutex'; + /** + * @var integer the permission to be set for newly created mutex files. + * This value will be used by PHP chmod() function. No umask will be applied. + * If not set, the permission will be determined by the current environment. + */ + public $fileMode; + /** + * @var integer the permission to be set for newly created directories. + * This value will be used by PHP chmod() function. No umask will be applied. + * Defaults to 0775, meaning the directory is read-writable by owner and group, + * but read-only for other users. + */ + public $dirMode = 0775; + /** + * @var resource[] stores all opened lock files. Keys are lock names and values are file handles. + */ + private $_files = []; + + + /** + * Initializes mutex component implementation dedicated for UNIX, GNU/Linux, Mac OS X, and other UNIX-like + * operating systems. + * @throws InvalidConfigException + */ + public function init() + { + if (stripos(php_uname('s'), 'win') === 0) { + throw new InvalidConfigException('FileMutex does not have MS Windows operating system support.'); + } + $this->mutexPath = Yii::getAlias($this->mutexPath); + if (!is_dir($this->mutexPath)) { + FileHelper::createDirectory($this->mutexPath, $this->dirMode, true); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + protected function acquireLock($name, $timeout = 0) + { + $fileName = $this->mutexPath . '/' . md5($name) . '.lock'; + $file = fopen($fileName, 'w+'); + if ($file === false) { + return false; + } + if ($this->fileMode !== null) { + @chmod($fileName, $this->fileMode); + } + $waitTime = 0; + while (!flock($file, LOCK_EX | LOCK_NB)) { + $waitTime++; + if ($waitTime > $timeout) { + fclose($file); + return false; + } + sleep(1); + } + $this->_files[$name] = $file; + return true; + } + + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + protected function releaseLock($name) + { + if (!isset($this->_files[$name]) || !flock($this->_files[$name], LOCK_UN)) { + return false; + } else { + fclose($this->_files[$name]); + unset($this->_files[$name]); + return true; + } + } +} diff --git a/framework/yii/mutex/Mutex.php b/framework/yii/mutex/Mutex.php new file mode 100644 index 0000000..611e725 --- /dev/null +++ b/framework/yii/mutex/Mutex.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +abstract class Mutex extends Component +{ + /** + * @var boolean whether all locks acquired in this process (i.e. local locks) must be released automagically + * before finishing script execution. Defaults to true. Setting this property to true means that all locks + * acquire in this process must be released in any case (regardless any kind of errors or exceptions). + */ + public $autoRelease = true; + /** + * @var string[] names of the locks acquired in the current PHP process. + */ + private $_locks = []; + + + /** + * Initializes the mutex component. + */ + public function init() + { + if ($this->autoRelease) { + $locks = &$this->_locks; + register_shutdown_function(function () use (&$locks) { + foreach ($locks as $lock) { + $this->release($lock); + } + }); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. Must be unique. + * @param integer $timeout to wait for lock to be released. Defaults to zero meaning that method will return + * false immediately in case lock was already acquired. + * @return boolean lock acquiring result. + */ + public function acquire($name, $timeout = 0) + { + if ($this->acquireLock($name, $timeout)) { + $this->_locks[] = $name; + return true; + } else { + return false; + } + } + + /** + * Release acquired lock. This method will return false in case named lock was not found. + * @param string $name of the lock to be released. This lock must be already created. + * @return boolean lock release result: false in case named lock was not found.. + */ + public function release($name) + { + if ($this->releaseLock($name)) { + $index = array_search($name, $this->_locks); + if ($index !== false) { + unset($this->_locks[$index]); + } + return true; + } else { + return false; + } + } + + /** + * This method should be extended by concrete mutex implementations. Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + */ + abstract protected function acquireLock($name, $timeout = 0); + + /** + * This method should be extended by concrete mutex implementations. Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + */ + abstract protected function releaseLock($name); +} diff --git a/framework/yii/mutex/MysqlMutex.php b/framework/yii/mutex/MysqlMutex.php new file mode 100644 index 0000000..af05b9c --- /dev/null +++ b/framework/yii/mutex/MysqlMutex.php @@ -0,0 +1,57 @@ + + * @since 2.0 + */ +class MysqlMutex extends Mutex +{ + /** + * Initializes MySQL specific mutex component implementation. + * @throws InvalidConfigException if [[db]] is not MySQL connection. + */ + public function init() + { + parent::init(); + if ($this->db->driverName !== 'mysql') { + throw new InvalidConfigException('In order to use MysqlMutex connection must be configured to use MySQL database.'); + } + } + + /** + * Acquires lock by given name. + * @param string $name of the lock to be acquired. + * @param integer $timeout to wait for lock to become released. + * @return boolean acquiring result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_get-lock + */ + protected function acquireLock($name, $timeout = 0) + { + return (boolean)$this->db + ->createCommand('SELECT GET_LOCK(:name, :timeout)', [':name' => $name, ':timeout' => $timeout]) + ->queryScalar(); + } + + /** + * Releases lock by given name. + * @param string $name of the lock to be released. + * @return boolean release result. + * @see http://dev.mysql.com/doc/refman/5.0/en/miscellaneous-functions.html#function_release-lock + */ + protected function releaseLock($name) + { + return (boolean)$this->db + ->createCommand('SELECT RELEASE_LOCK(:name)', [':name' => $name]) + ->queryScalar(); + } +} From baf6de3c0caa2ee8525e43e3c26d0d8b0be798c4 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Sat, 2 Nov 2013 14:52:24 -0400 Subject: [PATCH 044/109] Adjusted jui directories. --- extensions/jui/Accordion.php | 121 ++ extensions/jui/AccordionAsset.php | 26 + extensions/jui/AutoComplete.php | 66 + extensions/jui/AutoCompleteAsset.php | 25 + extensions/jui/ButtonAsset.php | 24 + extensions/jui/CoreAsset.php | 27 + extensions/jui/DatePicker.php | 110 + extensions/jui/DatePickerAsset.php | 25 + extensions/jui/DatePickerRegionalAsset.php | 24 + extensions/jui/Dialog.php | 52 + extensions/jui/DialogAsset.php | 27 + extensions/jui/Draggable.php | 50 + extensions/jui/DraggableAsset.php | 24 + extensions/jui/Droppable.php | 50 + extensions/jui/DroppableAsset.php | 24 + extensions/jui/EffectAsset.php | 24 + extensions/jui/Extension.php | 27 + extensions/jui/InputWidget.php | 59 + extensions/jui/Menu.php | 78 + extensions/jui/MenuAsset.php | 24 + extensions/jui/ProgressBar.php | 60 + extensions/jui/ProgressBarAsset.php | 24 + extensions/jui/Resizable.php | 52 + extensions/jui/ResizableAsset.php | 24 + extensions/jui/Selectable.php | 116 + extensions/jui/SelectableAsset.php | 24 + extensions/jui/Slider.php | 68 + extensions/jui/SliderAsset.php | 24 + extensions/jui/Sortable.php | 106 + extensions/jui/SortableAsset.php | 24 + extensions/jui/Spinner.php | 62 + extensions/jui/SpinnerAsset.php | 25 + extensions/jui/Tabs.php | 145 ++ extensions/jui/TabsAsset.php | 25 + extensions/jui/ThemeAsset.php | 21 + extensions/jui/TooltipAsset.php | 25 + extensions/jui/Widget.php | 87 + extensions/jui/assets.php | 23 + extensions/jui/assets/UPGRADE.md | 14 + extensions/jui/assets/jquery.ui.accordion.js | 572 +++++ extensions/jui/assets/jquery.ui.autocomplete.js | 610 ++++++ extensions/jui/assets/jquery.ui.button.js | 419 ++++ extensions/jui/assets/jquery.ui.core.js | 320 +++ extensions/jui/assets/jquery.ui.datepicker-i18n.js | 1793 ++++++++++++++++ extensions/jui/assets/jquery.ui.datepicker.js | 2038 ++++++++++++++++++ extensions/jui/assets/jquery.ui.dialog.js | 808 +++++++ extensions/jui/assets/jquery.ui.draggable.js | 958 +++++++++ extensions/jui/assets/jquery.ui.droppable.js | 372 ++++ extensions/jui/assets/jquery.ui.effect-all.js | 2261 ++++++++++++++++++++ extensions/jui/assets/jquery.ui.menu.js | 621 ++++++ extensions/jui/assets/jquery.ui.mouse.js | 169 ++ extensions/jui/assets/jquery.ui.position.js | 497 +++++ extensions/jui/assets/jquery.ui.progressbar.js | 145 ++ extensions/jui/assets/jquery.ui.resizable.js | 968 +++++++++ extensions/jui/assets/jquery.ui.selectable.js | 277 +++ extensions/jui/assets/jquery.ui.slider.js | 672 ++++++ extensions/jui/assets/jquery.ui.sortable.js | 1285 +++++++++++ extensions/jui/assets/jquery.ui.spinner.js | 493 +++++ extensions/jui/assets/jquery.ui.tabs.js | 846 ++++++++ extensions/jui/assets/jquery.ui.tooltip.js | 402 ++++ extensions/jui/assets/jquery.ui.widget.js | 521 +++++ .../jui/assets/theme/images/animated-overlay.gif | Bin 0 -> 1738 bytes .../theme/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 212 bytes .../theme/images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 208 bytes .../theme/images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 335 bytes .../theme/images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 207 bytes .../theme/images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 262 bytes .../theme/images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 262 bytes .../theme/images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 332 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 280 bytes .../theme/images/ui-icons_222222_256x240.png | Bin 0 -> 6922 bytes .../theme/images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4549 bytes .../theme/images/ui-icons_454545_256x240.png | Bin 0 -> 6992 bytes .../theme/images/ui-icons_888888_256x240.png | Bin 0 -> 6999 bytes .../theme/images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4549 bytes extensions/jui/assets/theme/jquery.ui.css | 1177 ++++++++++ extensions/jui/composer.json | 1 + extensions/jui/yii/jui/Accordion.php | 121 -- extensions/jui/yii/jui/AccordionAsset.php | 26 - extensions/jui/yii/jui/AutoComplete.php | 66 - extensions/jui/yii/jui/AutoCompleteAsset.php | 25 - extensions/jui/yii/jui/ButtonAsset.php | 24 - extensions/jui/yii/jui/CoreAsset.php | 27 - extensions/jui/yii/jui/DatePicker.php | 110 - extensions/jui/yii/jui/DatePickerAsset.php | 25 - extensions/jui/yii/jui/DatePickerRegionalAsset.php | 24 - extensions/jui/yii/jui/Dialog.php | 52 - extensions/jui/yii/jui/DialogAsset.php | 27 - extensions/jui/yii/jui/Draggable.php | 50 - extensions/jui/yii/jui/DraggableAsset.php | 24 - extensions/jui/yii/jui/Droppable.php | 50 - extensions/jui/yii/jui/DroppableAsset.php | 24 - extensions/jui/yii/jui/EffectAsset.php | 24 - extensions/jui/yii/jui/Extension.php | 27 - extensions/jui/yii/jui/InputWidget.php | 59 - extensions/jui/yii/jui/Menu.php | 78 - extensions/jui/yii/jui/MenuAsset.php | 24 - extensions/jui/yii/jui/ProgressBar.php | 60 - extensions/jui/yii/jui/ProgressBarAsset.php | 24 - extensions/jui/yii/jui/Resizable.php | 52 - extensions/jui/yii/jui/ResizableAsset.php | 24 - extensions/jui/yii/jui/Selectable.php | 116 - extensions/jui/yii/jui/SelectableAsset.php | 24 - extensions/jui/yii/jui/Slider.php | 68 - extensions/jui/yii/jui/SliderAsset.php | 24 - extensions/jui/yii/jui/Sortable.php | 106 - extensions/jui/yii/jui/SortableAsset.php | 24 - extensions/jui/yii/jui/Spinner.php | 62 - extensions/jui/yii/jui/SpinnerAsset.php | 25 - extensions/jui/yii/jui/Tabs.php | 145 -- extensions/jui/yii/jui/TabsAsset.php | 25 - extensions/jui/yii/jui/ThemeAsset.php | 21 - extensions/jui/yii/jui/TooltipAsset.php | 25 - extensions/jui/yii/jui/Widget.php | 87 - extensions/jui/yii/jui/assets.php | 23 - extensions/jui/yii/jui/assets/UPGRADE.md | 14 - .../jui/yii/jui/assets/jquery.ui.accordion.js | 572 ----- .../jui/yii/jui/assets/jquery.ui.autocomplete.js | 610 ------ extensions/jui/yii/jui/assets/jquery.ui.button.js | 419 ---- extensions/jui/yii/jui/assets/jquery.ui.core.js | 320 --- .../yii/jui/assets/jquery.ui.datepicker-i18n.js | 1793 ---------------- .../jui/yii/jui/assets/jquery.ui.datepicker.js | 2038 ------------------ extensions/jui/yii/jui/assets/jquery.ui.dialog.js | 808 ------- .../jui/yii/jui/assets/jquery.ui.draggable.js | 958 --------- .../jui/yii/jui/assets/jquery.ui.droppable.js | 372 ---- .../jui/yii/jui/assets/jquery.ui.effect-all.js | 2261 -------------------- extensions/jui/yii/jui/assets/jquery.ui.menu.js | 621 ------ extensions/jui/yii/jui/assets/jquery.ui.mouse.js | 169 -- .../jui/yii/jui/assets/jquery.ui.position.js | 497 ----- .../jui/yii/jui/assets/jquery.ui.progressbar.js | 145 -- .../jui/yii/jui/assets/jquery.ui.resizable.js | 968 --------- .../jui/yii/jui/assets/jquery.ui.selectable.js | 277 --- extensions/jui/yii/jui/assets/jquery.ui.slider.js | 672 ------ .../jui/yii/jui/assets/jquery.ui.sortable.js | 1285 ----------- extensions/jui/yii/jui/assets/jquery.ui.spinner.js | 493 ----- extensions/jui/yii/jui/assets/jquery.ui.tabs.js | 846 -------- extensions/jui/yii/jui/assets/jquery.ui.tooltip.js | 402 ---- extensions/jui/yii/jui/assets/jquery.ui.widget.js | 521 ----- .../jui/assets/theme/images/animated-overlay.gif | Bin 1738 -> 0 bytes .../theme/images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 212 -> 0 bytes .../theme/images/ui-bg_flat_75_ffffff_40x100.png | Bin 208 -> 0 bytes .../theme/images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 335 -> 0 bytes .../theme/images/ui-bg_glass_65_ffffff_1x400.png | Bin 207 -> 0 bytes .../theme/images/ui-bg_glass_75_dadada_1x400.png | Bin 262 -> 0 bytes .../theme/images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 262 -> 0 bytes .../theme/images/ui-bg_glass_95_fef1ec_1x400.png | Bin 332 -> 0 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 280 -> 0 bytes .../theme/images/ui-icons_222222_256x240.png | Bin 6922 -> 0 bytes .../theme/images/ui-icons_2e83ff_256x240.png | Bin 4549 -> 0 bytes .../theme/images/ui-icons_454545_256x240.png | Bin 6992 -> 0 bytes .../theme/images/ui-icons_888888_256x240.png | Bin 6999 -> 0 bytes .../theme/images/ui-icons_cd0a0a_256x240.png | Bin 4549 -> 0 bytes extensions/jui/yii/jui/assets/theme/jquery.ui.css | 1177 ---------- 153 files changed, 20061 insertions(+), 20060 deletions(-) create mode 100644 extensions/jui/Accordion.php create mode 100644 extensions/jui/AccordionAsset.php create mode 100644 extensions/jui/AutoComplete.php create mode 100644 extensions/jui/AutoCompleteAsset.php create mode 100644 extensions/jui/ButtonAsset.php create mode 100644 extensions/jui/CoreAsset.php create mode 100644 extensions/jui/DatePicker.php create mode 100644 extensions/jui/DatePickerAsset.php create mode 100644 extensions/jui/DatePickerRegionalAsset.php create mode 100644 extensions/jui/Dialog.php create mode 100644 extensions/jui/DialogAsset.php create mode 100644 extensions/jui/Draggable.php create mode 100644 extensions/jui/DraggableAsset.php create mode 100644 extensions/jui/Droppable.php create mode 100644 extensions/jui/DroppableAsset.php create mode 100644 extensions/jui/EffectAsset.php create mode 100644 extensions/jui/Extension.php create mode 100644 extensions/jui/InputWidget.php create mode 100644 extensions/jui/Menu.php create mode 100644 extensions/jui/MenuAsset.php create mode 100644 extensions/jui/ProgressBar.php create mode 100644 extensions/jui/ProgressBarAsset.php create mode 100644 extensions/jui/Resizable.php create mode 100644 extensions/jui/ResizableAsset.php create mode 100644 extensions/jui/Selectable.php create mode 100644 extensions/jui/SelectableAsset.php create mode 100644 extensions/jui/Slider.php create mode 100644 extensions/jui/SliderAsset.php create mode 100644 extensions/jui/Sortable.php create mode 100644 extensions/jui/SortableAsset.php create mode 100644 extensions/jui/Spinner.php create mode 100644 extensions/jui/SpinnerAsset.php create mode 100644 extensions/jui/Tabs.php create mode 100644 extensions/jui/TabsAsset.php create mode 100644 extensions/jui/ThemeAsset.php create mode 100644 extensions/jui/TooltipAsset.php create mode 100644 extensions/jui/Widget.php create mode 100644 extensions/jui/assets.php create mode 100644 extensions/jui/assets/UPGRADE.md create mode 100644 extensions/jui/assets/jquery.ui.accordion.js create mode 100644 extensions/jui/assets/jquery.ui.autocomplete.js create mode 100644 extensions/jui/assets/jquery.ui.button.js create mode 100644 extensions/jui/assets/jquery.ui.core.js create mode 100755 extensions/jui/assets/jquery.ui.datepicker-i18n.js create mode 100644 extensions/jui/assets/jquery.ui.datepicker.js create mode 100644 extensions/jui/assets/jquery.ui.dialog.js create mode 100644 extensions/jui/assets/jquery.ui.draggable.js create mode 100644 extensions/jui/assets/jquery.ui.droppable.js create mode 100755 extensions/jui/assets/jquery.ui.effect-all.js create mode 100644 extensions/jui/assets/jquery.ui.menu.js create mode 100644 extensions/jui/assets/jquery.ui.mouse.js create mode 100644 extensions/jui/assets/jquery.ui.position.js create mode 100644 extensions/jui/assets/jquery.ui.progressbar.js create mode 100644 extensions/jui/assets/jquery.ui.resizable.js create mode 100644 extensions/jui/assets/jquery.ui.selectable.js create mode 100644 extensions/jui/assets/jquery.ui.slider.js create mode 100644 extensions/jui/assets/jquery.ui.sortable.js create mode 100644 extensions/jui/assets/jquery.ui.spinner.js create mode 100644 extensions/jui/assets/jquery.ui.tabs.js create mode 100644 extensions/jui/assets/jquery.ui.tooltip.js create mode 100644 extensions/jui/assets/jquery.ui.widget.js create mode 100755 extensions/jui/assets/theme/images/animated-overlay.gif create mode 100755 extensions/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100755 extensions/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100755 extensions/jui/assets/theme/images/ui-icons_222222_256x240.png create mode 100755 extensions/jui/assets/theme/images/ui-icons_2e83ff_256x240.png create mode 100755 extensions/jui/assets/theme/images/ui-icons_454545_256x240.png create mode 100755 extensions/jui/assets/theme/images/ui-icons_888888_256x240.png create mode 100755 extensions/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png create mode 100755 extensions/jui/assets/theme/jquery.ui.css delete mode 100644 extensions/jui/yii/jui/Accordion.php delete mode 100644 extensions/jui/yii/jui/AccordionAsset.php delete mode 100644 extensions/jui/yii/jui/AutoComplete.php delete mode 100644 extensions/jui/yii/jui/AutoCompleteAsset.php delete mode 100644 extensions/jui/yii/jui/ButtonAsset.php delete mode 100644 extensions/jui/yii/jui/CoreAsset.php delete mode 100644 extensions/jui/yii/jui/DatePicker.php delete mode 100644 extensions/jui/yii/jui/DatePickerAsset.php delete mode 100644 extensions/jui/yii/jui/DatePickerRegionalAsset.php delete mode 100644 extensions/jui/yii/jui/Dialog.php delete mode 100644 extensions/jui/yii/jui/DialogAsset.php delete mode 100644 extensions/jui/yii/jui/Draggable.php delete mode 100644 extensions/jui/yii/jui/DraggableAsset.php delete mode 100644 extensions/jui/yii/jui/Droppable.php delete mode 100644 extensions/jui/yii/jui/DroppableAsset.php delete mode 100644 extensions/jui/yii/jui/EffectAsset.php delete mode 100644 extensions/jui/yii/jui/Extension.php delete mode 100644 extensions/jui/yii/jui/InputWidget.php delete mode 100644 extensions/jui/yii/jui/Menu.php delete mode 100644 extensions/jui/yii/jui/MenuAsset.php delete mode 100644 extensions/jui/yii/jui/ProgressBar.php delete mode 100644 extensions/jui/yii/jui/ProgressBarAsset.php delete mode 100644 extensions/jui/yii/jui/Resizable.php delete mode 100644 extensions/jui/yii/jui/ResizableAsset.php delete mode 100644 extensions/jui/yii/jui/Selectable.php delete mode 100644 extensions/jui/yii/jui/SelectableAsset.php delete mode 100644 extensions/jui/yii/jui/Slider.php delete mode 100644 extensions/jui/yii/jui/SliderAsset.php delete mode 100644 extensions/jui/yii/jui/Sortable.php delete mode 100644 extensions/jui/yii/jui/SortableAsset.php delete mode 100644 extensions/jui/yii/jui/Spinner.php delete mode 100644 extensions/jui/yii/jui/SpinnerAsset.php delete mode 100644 extensions/jui/yii/jui/Tabs.php delete mode 100644 extensions/jui/yii/jui/TabsAsset.php delete mode 100644 extensions/jui/yii/jui/ThemeAsset.php delete mode 100644 extensions/jui/yii/jui/TooltipAsset.php delete mode 100644 extensions/jui/yii/jui/Widget.php delete mode 100644 extensions/jui/yii/jui/assets.php delete mode 100644 extensions/jui/yii/jui/assets/UPGRADE.md delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.accordion.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.autocomplete.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.button.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.core.js delete mode 100755 extensions/jui/yii/jui/assets/jquery.ui.datepicker-i18n.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.datepicker.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.dialog.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.draggable.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.droppable.js delete mode 100755 extensions/jui/yii/jui/assets/jquery.ui.effect-all.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.menu.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.mouse.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.position.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.progressbar.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.resizable.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.selectable.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.slider.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.sortable.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.spinner.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.tabs.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.tooltip.js delete mode 100644 extensions/jui/yii/jui/assets/jquery.ui.widget.js delete mode 100755 extensions/jui/yii/jui/assets/theme/images/animated-overlay.gif delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_0_aaaaaa_40x100.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_flat_75_ffffff_40x100.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_55_fbf9ee_1x400.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_65_ffffff_1x400.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_dadada_1x400.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_75_e6e6e6_1x400.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_glass_95_fef1ec_1x400.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-bg_highlight-soft_75_cccccc_1x100.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-icons_222222_256x240.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-icons_2e83ff_256x240.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-icons_454545_256x240.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-icons_888888_256x240.png delete mode 100755 extensions/jui/yii/jui/assets/theme/images/ui-icons_cd0a0a_256x240.png delete mode 100755 extensions/jui/yii/jui/assets/theme/jquery.ui.css diff --git a/extensions/jui/Accordion.php b/extensions/jui/Accordion.php new file mode 100644 index 0000000..42897a9 --- /dev/null +++ b/extensions/jui/Accordion.php @@ -0,0 +1,121 @@ + [ + * [ + * 'header' => 'Section 1', + * 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...', + * ], + * [ + * 'header' => 'Section 2', + * 'headerOptions' => ['tag' => 'h3'], + * 'content' => 'Sed non urna. Phasellus eu ligula. Vestibulum sit amet purus...', + * 'options' => ['tag' => 'div'], + * ], + * ], + * 'options' => ['tag' => 'div'], + * 'itemOptions' => ['tag' => 'div'], + * 'headerOptions' => ['tag' => 'h3'], + * 'clientOptions' => ['collapsible' => false], + * ]); + * ``` + * + * @see http://api.jqueryui.com/accordion/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Accordion extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + */ + public $options = []; + /** + * @var array list of collapsible items. Each item can be an array of the following structure: + * + * ~~~ + * [ + * 'header' => 'Item header', + * 'content' => 'Item content', + * // the HTML attributes of the item header container tag. This will overwrite "headerOptions". + * 'headerOptions' => [], + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the item header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "h3", the tag name of the item container tags. + */ + public $headerOptions = []; + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('accordion', AccordionAsset::className()); + } + + /** + * Renders collapsible items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $items = []; + foreach ($this->items as $item) { + if (!isset($item['header'])) { + throw new InvalidConfigException("The 'header' option is required."); + } + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); + $headerTag = ArrayHelper::remove($headerOptions, 'tag', 'h3'); + $items[] = Html::tag($headerTag, $item['header'], $headerOptions); + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + $items[] = Html::tag($tag, $item['content'], $options); + } + + return implode("\n", $items); + } +} diff --git a/extensions/jui/AccordionAsset.php b/extensions/jui/AccordionAsset.php new file mode 100644 index 0000000..05c1e20 --- /dev/null +++ b/extensions/jui/AccordionAsset.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class AccordionAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.accordion.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; +} diff --git a/extensions/jui/AutoComplete.php b/extensions/jui/AutoComplete.php new file mode 100644 index 0000000..ac0c997 --- /dev/null +++ b/extensions/jui/AutoComplete.php @@ -0,0 +1,66 @@ + $model, + * 'attribute' => 'country', + * 'clientOptions' => [ + * 'source' => ['USA', 'RUS'], + * ], + * ]); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo AutoComplete::widget([ + * 'name' => 'country', + * 'clientOptions' => [ + * 'source' => ['USA', 'RUS'], + * ], + * ]); + *``` + * + * @see http://api.jqueryui.com/autocomplete/ + * @author Alexander Kochetov + * @since 2.0 + */ +class AutoComplete extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('autocomplete', AutoCompleteAsset::className()); + } + + /** + * Renders the AutoComplete widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/AutoCompleteAsset.php b/extensions/jui/AutoCompleteAsset.php new file mode 100644 index 0000000..f48e064 --- /dev/null +++ b/extensions/jui/AutoCompleteAsset.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +class AutoCompleteAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.autocomplete.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\MenuAsset', + ]; +} diff --git a/extensions/jui/ButtonAsset.php b/extensions/jui/ButtonAsset.php new file mode 100644 index 0000000..6616b34 --- /dev/null +++ b/extensions/jui/ButtonAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class ButtonAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.button.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/CoreAsset.php b/extensions/jui/CoreAsset.php new file mode 100644 index 0000000..d77a25f --- /dev/null +++ b/extensions/jui/CoreAsset.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class CoreAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.core.js', + 'jquery.ui.widget.js', + 'jquery.ui.position.js', + 'jquery.ui.mouse.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + ]; +} diff --git a/extensions/jui/DatePicker.php b/extensions/jui/DatePicker.php new file mode 100644 index 0000000..06ca356 --- /dev/null +++ b/extensions/jui/DatePicker.php @@ -0,0 +1,110 @@ + 'ru', + * 'model' => $model, + * 'attribute' => 'country', + * 'clientOptions' => [ + * 'dateFormat' => 'yy-mm-dd', + * ], + * ]); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo DatePicker::widget([ + * 'language' => 'ru', + * 'name' => 'country', + * 'clientOptions' => [ + * 'dateFormat' => 'yy-mm-dd', + * ], + * ]); + *``` + * + * @see http://api.jqueryui.com/datepicker/ + * @author Alexander Kochetov + * @since 2.0 + */ +class DatePicker extends InputWidget +{ + /** + * @var string the locale ID (eg 'fr', 'de') for the language to be used by the date picker. + * If this property set to false, I18N will not be involved. That is, the date picker will show in English. + */ + public $language = false; + /** + * @var boolean If true, shows the widget as an inline calendar and the input as a hidden field. + */ + public $inline = false; + + + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget() . "\n"; + if ($this->language !== false) { + $view = $this->getView(); + DatePickerRegionalAsset::register($view); + + $options = Json::encode($this->clientOptions); + $view->registerJs("$('#{$this->options['id']}').datepicker($.extend({}, $.datepicker.regional['{$this->language}'], $options));"); + + $options = $this->clientOptions; + $this->clientOptions = false; // the datepicker js widget is already registered + $this->registerWidget('datepicker', DatePickerAsset::className()); + $this->clientOptions = $options; + } else { + $this->registerWidget('datepicker', DatePickerAsset::className()); + } + } + + /** + * Renders the DatePicker widget. + * @return string the rendering result. + */ + protected function renderWidget() + { + $contents = []; + + if ($this->inline === false) { + if ($this->hasModel()) { + $contents[] = Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + $contents[] = Html::textInput($this->name, $this->value, $this->options); + } + } else { + if ($this->hasModel()) { + $contents[] = Html::activeHiddenInput($this->model, $this->attribute, $this->options); + $this->clientOptions['defaultDate'] = $this->model->{$this->attribute}; + } else { + $contents[] = Html::hiddenInput($this->name, $this->value, $this->options); + $this->clientOptions['defaultDate'] = $this->value; + } + $this->clientOptions['altField'] = '#' . $this->options['id']; + $this->options['id'] .= '-container'; + $contents[] = Html::tag('div', null, $this->options); + } + + return implode("\n", $contents); + } +} diff --git a/extensions/jui/DatePickerAsset.php b/extensions/jui/DatePickerAsset.php new file mode 100644 index 0000000..fddd8df --- /dev/null +++ b/extensions/jui/DatePickerAsset.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +class DatePickerAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.datepicker.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; +} diff --git a/extensions/jui/DatePickerRegionalAsset.php b/extensions/jui/DatePickerRegionalAsset.php new file mode 100644 index 0000000..249373a --- /dev/null +++ b/extensions/jui/DatePickerRegionalAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class DatePickerRegionalAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.datepicker-i18n.js', + ]; + public $depends = [ + 'yii\jui\DatePickerAsset', + ]; +} diff --git a/extensions/jui/Dialog.php b/extensions/jui/Dialog.php new file mode 100644 index 0000000..a5cbaf2 --- /dev/null +++ b/extensions/jui/Dialog.php @@ -0,0 +1,52 @@ + [ + * 'modal' => true, + * ], + * ]); + * + * echo 'Dialog contents here...'; + * + * Dialog::end(); + * ``` + * + * @see http://api.jqueryui.com/dialog/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Dialog extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('dialog', DialogAsset::className()); + } +} diff --git a/extensions/jui/DialogAsset.php b/extensions/jui/DialogAsset.php new file mode 100644 index 0000000..109243e --- /dev/null +++ b/extensions/jui/DialogAsset.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class DialogAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.dialog.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + 'yii\jui\DraggableAsset', + 'yii\jui\ResizableAsset', + ]; +} diff --git a/extensions/jui/Draggable.php b/extensions/jui/Draggable.php new file mode 100644 index 0000000..02e4973 --- /dev/null +++ b/extensions/jui/Draggable.php @@ -0,0 +1,50 @@ + ['grid' => [50, 20]], + * ]); + * + * echo 'Draggable contents here...'; + * + * Draggable::end(); + * ``` + * + * @see http://api.jqueryui.com/draggable/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Draggable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('draggable', DraggableAsset::className()); + } +} diff --git a/extensions/jui/DraggableAsset.php b/extensions/jui/DraggableAsset.php new file mode 100644 index 0000000..f3286a5 --- /dev/null +++ b/extensions/jui/DraggableAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class DraggableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.draggable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Droppable.php b/extensions/jui/Droppable.php new file mode 100644 index 0000000..530e736 --- /dev/null +++ b/extensions/jui/Droppable.php @@ -0,0 +1,50 @@ + ['accept' => '.special'], + * ]); + * + * echo 'Droppable body here...'; + * + * Droppable::end(); + * ``` + * + * @see http://api.jqueryui.com/droppable/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Droppable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('droppable', DroppableAsset::className()); + } +} diff --git a/extensions/jui/DroppableAsset.php b/extensions/jui/DroppableAsset.php new file mode 100644 index 0000000..84b64b8 --- /dev/null +++ b/extensions/jui/DroppableAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class DroppableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.droppable.js', + ]; + public $depends = [ + 'yii\jui\DraggableAsset', + ]; +} diff --git a/extensions/jui/EffectAsset.php b/extensions/jui/EffectAsset.php new file mode 100644 index 0000000..79c5aaa --- /dev/null +++ b/extensions/jui/EffectAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class EffectAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.effect-all.js', + ]; + public $depends = [ + 'yii\web\JqueryAsset', + ]; +} diff --git a/extensions/jui/Extension.php b/extensions/jui/Extension.php new file mode 100644 index 0000000..4b680ce --- /dev/null +++ b/extensions/jui/Extension.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +class Extension extends \yii\base\Extension +{ + /** + * @inheritdoc + */ + public static function init() + { + Yii::setAlias('@yii/jui', __DIR__); + } +} diff --git a/extensions/jui/InputWidget.php b/extensions/jui/InputWidget.php new file mode 100644 index 0000000..e100d6c --- /dev/null +++ b/extensions/jui/InputWidget.php @@ -0,0 +1,59 @@ + + * @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/extensions/jui/Menu.php b/extensions/jui/Menu.php new file mode 100644 index 0000000..46c7ee0 --- /dev/null +++ b/extensions/jui/Menu.php @@ -0,0 +1,78 @@ + + * @since 2.0 + */ +class Menu extends \yii\widgets\Menu +{ + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + */ + public $clientEvents = []; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Renders the widget. + */ + public function run() + { + parent::run(); + + $view = $this->getView(); + MenuAsset::register($view); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = Widget::$theme; + $themeAsset::register($view); + + $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').menu($options);"; + $view->registerJs($js); + } + + if (!empty($this->clientEvents)) { + $js = []; + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('menu$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } +} diff --git a/extensions/jui/MenuAsset.php b/extensions/jui/MenuAsset.php new file mode 100644 index 0000000..8b840a8 --- /dev/null +++ b/extensions/jui/MenuAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class MenuAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.menu.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/ProgressBar.php b/extensions/jui/ProgressBar.php new file mode 100644 index 0000000..1c555f1 --- /dev/null +++ b/extensions/jui/ProgressBar.php @@ -0,0 +1,60 @@ + [ + * 'value' => 75, + * ], + * ]); + * ``` + * + * The following example will show the content enclosed between the [[begin()]] + * and [[end()]] calls within the widget container: + * + * ~~~php + * ProgressBar::widget([ + * 'clientOptions' => ['value' => 75], + * ]); + * + * echo '
Loading...
'; + * + * ProgressBar::end(); + * ~~~ + * @see http://api.jqueryui.com/progressbar/ + * @author Alexander Kochetov + * @since 2.0 + */ +class ProgressBar extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('progressbar', ProgressBarAsset::className()); + } +} diff --git a/extensions/jui/ProgressBarAsset.php b/extensions/jui/ProgressBarAsset.php new file mode 100644 index 0000000..d485fbd --- /dev/null +++ b/extensions/jui/ProgressBarAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class ProgressBarAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.progressbar.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Resizable.php b/extensions/jui/Resizable.php new file mode 100644 index 0000000..bcff9a8 --- /dev/null +++ b/extensions/jui/Resizable.php @@ -0,0 +1,52 @@ + [ + * 'grid' => [20, 10], + * ], + * ]); + * + * echo 'Resizable contents here...'; + * + * Resizable::end(); + * ``` + * + * @see http://api.jqueryui.com/resizable/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Resizable extends Widget +{ + /** + * Initializes the widget. + */ + public function init() + { + parent::init(); + echo Html::beginTag('div', $this->options) . "\n"; + } + + /** + * Renders the widget. + */ + public function run() + { + echo Html::endTag('div') . "\n"; + $this->registerWidget('resizable', ResizableAsset::className()); + } +} diff --git a/extensions/jui/ResizableAsset.php b/extensions/jui/ResizableAsset.php new file mode 100644 index 0000000..acf4c73 --- /dev/null +++ b/extensions/jui/ResizableAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class ResizableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.resizable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Selectable.php b/extensions/jui/Selectable.php new file mode 100644 index 0000000..94e4faf --- /dev/null +++ b/extensions/jui/Selectable.php @@ -0,0 +1,116 @@ + [ + * 'Item 1', + * [ + * 'content' => 'Item2', + * ], + * [ + * 'content' => 'Item3', + * 'options' => [ + * 'tag' => 'li', + * ], + * ], + * ), + * 'options' => [ + * 'tag' => 'ul', + * ], + * 'itemOptions' => [ + * 'tag' => 'li', + * ], + * 'clientOptions' => [ + * 'tolerance' => 'fit', + * ], + * ]); + * ``` + * + * @see http://api.jqueryui.com/selectable/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Selectable extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget + */ + public $options = []; + /** + * @var array list of selectable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * [ + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + */ + public $itemOptions = []; + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('selectable', SelectableAsset::className()); + } + + /** + * Renders selectable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } + return implode("\n", $items); + } +} diff --git a/extensions/jui/SelectableAsset.php b/extensions/jui/SelectableAsset.php new file mode 100644 index 0000000..61f405f --- /dev/null +++ b/extensions/jui/SelectableAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class SelectableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.selectable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Slider.php b/extensions/jui/Slider.php new file mode 100644 index 0000000..c19f2db --- /dev/null +++ b/extensions/jui/Slider.php @@ -0,0 +1,68 @@ + $model, + * 'attribute' => 'amount', + * 'clientOptions' => [ + * 'min' => 1, + * 'max' => 10, + * ], + * ]); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo Slider::widget([ + * 'name' => 'amount', + * 'clientOptions' => [ + * 'min' => 1, + * 'max' => 10, + * ], + * ]); + *``` + * + * @see http://api.jqueryui.com/slider/ + * @author Alexander Makarov + * @since 2.0 + */ +class Slider extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('slider', SliderAsset::className()); + } + + /** + * Renders the Slider widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/SliderAsset.php b/extensions/jui/SliderAsset.php new file mode 100644 index 0000000..56c2451 --- /dev/null +++ b/extensions/jui/SliderAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class SliderAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.slider.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Sortable.php b/extensions/jui/Sortable.php new file mode 100644 index 0000000..6209cb6 --- /dev/null +++ b/extensions/jui/Sortable.php @@ -0,0 +1,106 @@ + [ + * 'Item 1', + * ['content' => 'Item2'], + * [ + * 'content' => 'Item3', + * 'options' => ['tag' => 'li'], + * ], + * ], + * 'options' => ['tag' => 'ul'], + * 'itemOptions' => ['tag' => 'li'], + * 'clientOptions' => ['cursor' => 'move'], + * )); + * ``` + * + * @see http://api.jqueryui.com/sortable/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Sortable extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "ul", the tag name of the container tag of this widget + */ + public $options = []; + /** + * @var array list of sortable items. Each item can be a string representing the item content + * or an array of the following structure: + * + * ~~~ + * [ + * 'content' => 'item content', + * // the HTML attributes of the item container tag. This will overwrite "itemOptions". + * 'options' => [], + * ] + * ~~~ + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "li", the tag name of the item container tags. + */ + public $itemOptions = []; + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'ul'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('sortable', SortableAsset::className()); + } + + /** + * Renders sortable items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + public function renderItems() + { + $items = []; + foreach ($this->items as $item) { + $options = $this->itemOptions; + $tag = ArrayHelper::remove($options, 'tag', 'li'); + if (is_array($item)) { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' option is required."); + } + $options = array_merge($options, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', $tag); + $items[] = Html::tag($tag, $item['content'], $options); + } else { + $items[] = Html::tag($tag, $item, $options); + } + } + return implode("\n", $items); + } +} diff --git a/extensions/jui/SortableAsset.php b/extensions/jui/SortableAsset.php new file mode 100644 index 0000000..69c9ba3 --- /dev/null +++ b/extensions/jui/SortableAsset.php @@ -0,0 +1,24 @@ + + * @since 2.0 + */ +class SortableAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.sortable.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + ]; +} diff --git a/extensions/jui/Spinner.php b/extensions/jui/Spinner.php new file mode 100644 index 0000000..caf73f3 --- /dev/null +++ b/extensions/jui/Spinner.php @@ -0,0 +1,62 @@ + $model, + * 'attribute' => 'country', + * 'clientOptions' => ['step' => 2], + * ]); + * ``` + * + * The following example will use the name property instead: + * + * ```php + * echo Spinner::widget([ + * 'name' => 'country', + * 'clientOptions' => ['step' => 2], + * ]); + *``` + * + * @see http://api.jqueryui.com/spinner/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Spinner extends InputWidget +{ + /** + * Renders the widget. + */ + public function run() + { + echo $this->renderWidget(); + $this->registerWidget('spinner', SpinnerAsset::className()); + } + + /** + * Renders the Spinner widget. + * @return string the rendering result. + */ + public function renderWidget() + { + if ($this->hasModel()) { + return Html::activeTextInput($this->model, $this->attribute, $this->options); + } else { + return Html::textInput($this->name, $this->value, $this->options); + } + } +} diff --git a/extensions/jui/SpinnerAsset.php b/extensions/jui/SpinnerAsset.php new file mode 100644 index 0000000..89a8c59 --- /dev/null +++ b/extensions/jui/SpinnerAsset.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +class SpinnerAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.spinner.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\ButtonAsset', + ]; +} diff --git a/extensions/jui/Tabs.php b/extensions/jui/Tabs.php new file mode 100644 index 0000000..9d2a2be --- /dev/null +++ b/extensions/jui/Tabs.php @@ -0,0 +1,145 @@ + [ + * [ + * 'label' => 'Tab one', + * 'content' => 'Mauris mauris ante, blandit et, ultrices a, suscipit eget...', + * ], + * [ + * 'label' => 'Tab two', + * 'content' => 'Sed non urna. Phasellus eu ligula. Vestibulum sit amet purus...', + * 'options' => ['tag' => 'div'], + * 'headerOptions' => ['class' => 'my-class'], + * ], + * [ + * 'label' => 'Tab with custom id', + * 'content' => 'Morbi tincidunt, dui sit amet facilisis feugiat...', + * 'options' => ['id' => 'my-tab'], + * ], + * [ + * 'label' => 'Ajax tab', + * 'url' => ['ajax/content'], + * ], + * ), + * 'options' => ['tag' => 'div'], + * 'itemOptions' => ['tag' => 'div'], + * 'headerOptions' => ['class' => 'my-class'], + * 'clientOptions' => ['collapsible' => false], + * ]); + * ``` + * + * @see http://api.jqueryui.com/tabs/ + * @author Alexander Kochetov + * @since 2.0 + */ +class Tabs extends Widget +{ + /** + * @var array the HTML attributes for the widget container tag. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the container tag of this widget + */ + public $options = []; + /** + * @var array list of tab items. Each item can be an array of the following structure: + * + * - label: string, required, specifies the header link label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. + * - content: string, the content to show when corresponding tab is clicked. Can be omitted if url is specified. + * - url: mixed, mixed, optional, the url to load tab contents via AJAX. It is required if no content is specified. + * - template: string, optional, the header link template to render the header link. If none specified + * [[linkTemplate]] will be used instead. + * - options: array, optional, the HTML attributes of the header. + * - headerOptions: array, optional, the HTML attributes for the header container tag. + */ + public $items = []; + /** + * @var array list of HTML attributes for the item container tags. This will be overwritten + * by the "options" set in individual [[items]]. The following special options are recognized: + * + * - tag: string, defaults to "div", the tag name of the item container tags. + */ + public $itemOptions = []; + /** + * @var array list of HTML attributes for the header container tags. This will be overwritten + * by the "headerOptions" set in individual [[items]]. + */ + public $headerOptions = []; + /** + * @var string the default header template to render the link. + */ + public $linkTemplate = '{label}'; + /** + * @var boolean whether the labels for header items should be HTML-encoded. + */ + public $encodeLabels = true; + + + /** + * Renders the widget. + */ + public function run() + { + $options = $this->options; + $tag = ArrayHelper::remove($options, 'tag', 'div'); + echo Html::beginTag($tag, $options) . "\n"; + echo $this->renderItems() . "\n"; + echo Html::endTag($tag) . "\n"; + $this->registerWidget('tabs', TabsAsset::className()); + } + + /** + * Renders tab items as specified on [[items]]. + * @return string the rendering result. + * @throws InvalidConfigException. + */ + protected function renderItems() + { + $headers = []; + $items = []; + foreach ($this->items as $n => $item) { + if (!isset($item['label'])) { + throw new InvalidConfigException("The 'label' option is required."); + } + if (isset($item['url'])) { + $url = Html::url($item['url']); + } else { + if (!isset($item['content'])) { + throw new InvalidConfigException("The 'content' or 'url' option is required."); + } + $options = array_merge($this->itemOptions, ArrayHelper::getValue($item, 'options', [])); + $tag = ArrayHelper::remove($options, 'tag', 'div'); + if (!isset($options['id'])) { + $options['id'] = $this->options['id'] . '-tab' . $n; + } + $url = '#' . $options['id']; + $items[] = Html::tag($tag, $item['content'], $options); + } + $headerOptions = array_merge($this->headerOptions, ArrayHelper::getValue($item, 'headerOptions', [])); + $template = ArrayHelper::getValue($item, 'template', $this->linkTemplate); + $headers[] = Html::tag('li', strtr($template, [ + '{label}' => $this->encodeLabels ? Html::encode($item['label']) : $item['label'], + '{url}' => $url, + ]), $headerOptions); + } + return Html::tag('ul', implode("\n", $headers)) . "\n" . implode("\n", $items); + } +} diff --git a/extensions/jui/TabsAsset.php b/extensions/jui/TabsAsset.php new file mode 100644 index 0000000..5bef4c0 --- /dev/null +++ b/extensions/jui/TabsAsset.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +class TabsAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.tabs.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; +} diff --git a/extensions/jui/ThemeAsset.php b/extensions/jui/ThemeAsset.php new file mode 100644 index 0000000..dedcb00 --- /dev/null +++ b/extensions/jui/ThemeAsset.php @@ -0,0 +1,21 @@ + + * @since 2.0 + */ +class ThemeAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $css = [ + 'theme/jquery.ui.css', + ]; +} diff --git a/extensions/jui/TooltipAsset.php b/extensions/jui/TooltipAsset.php new file mode 100644 index 0000000..1fa4490 --- /dev/null +++ b/extensions/jui/TooltipAsset.php @@ -0,0 +1,25 @@ + + * @since 2.0 + */ +class TooltipAsset extends AssetBundle +{ + public $sourcePath = '@yii/jui/assets'; + public $js = [ + 'jquery.ui.tooltip.js', + ]; + public $depends = [ + 'yii\jui\CoreAsset', + 'yii\jui\EffectAsset', + ]; +} diff --git a/extensions/jui/Widget.php b/extensions/jui/Widget.php new file mode 100644 index 0000000..2bbc9e4 --- /dev/null +++ b/extensions/jui/Widget.php @@ -0,0 +1,87 @@ + + * @since 2.0 + */ +class Widget extends \yii\base\Widget +{ + /** + * @var string the jQuery UI theme. This refers to an asset bundle class + * representing the JUI theme. The default theme is the official "Smoothness" theme. + */ + public static $theme = 'yii\jui\ThemeAsset'; + /** + * @var array the HTML attributes for the widget container tag. + */ + public $options = []; + /** + * @var array the options for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible options. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported options (e.g. "header"). + */ + public $clientOptions = []; + /** + * @var array the event handlers for the underlying jQuery UI widget. + * Please refer to the corresponding jQuery UI widget Web page for possible events. + * For example, [this page](http://api.jqueryui.com/accordion/) shows + * how to use the "Accordion" widget and the supported events (e.g. "create"). + */ + public $clientEvents = []; + + + /** + * Initializes the widget. + * If you override this method, make sure you call the parent implementation first. + */ + public function init() + { + parent::init(); + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + } + + /** + * Registers a specific jQuery UI widget and the related events + * @param string $name the name of the jQuery UI widget + * @param string $assetBundle the asset bundle for the widget + */ + protected function registerWidget($name, $assetBundle) + { + $view = $this->getView(); + /** @var \yii\web\AssetBundle $assetBundle */ + $assetBundle::register($view); + /** @var \yii\web\AssetBundle $themeAsset */ + $themeAsset = self::$theme; + $themeAsset::register($view); + + $id = $this->options['id']; + if ($this->clientOptions !== false) { + $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); + $js = "jQuery('#$id').$name($options);"; + $view->registerJs($js); + } + + if (!empty($this->clientEvents)) { + $js = []; + foreach ($this->clientEvents as $event => $handler) { + $js[] = "jQuery('#$id').on('$name$event', $handler);"; + } + $view->registerJs(implode("\n", $js)); + } + } +} diff --git a/extensions/jui/assets.php b/extensions/jui/assets.php new file mode 100644 index 0000000..ab4c930 --- /dev/null +++ b/extensions/jui/assets.php @@ -0,0 +1,23 @@ + li > :first-child,> :not(li):even", + heightStyle: "auto", + icons: { + activeHeader: "ui-icon-triangle-1-s", + header: "ui-icon-triangle-1-e" + }, + + // callbacks + activate: null, + beforeActivate: null + }, + + _create: function() { + var options = this.options; + this.prevShow = this.prevHide = $(); + this.element.addClass( "ui-accordion ui-widget ui-helper-reset" ) + // ARIA + .attr( "role", "tablist" ); + + // don't allow collapsible: false and active: false / null + if ( !options.collapsible && (options.active === false || options.active == null) ) { + options.active = 0; + } + + this._processPanels(); + // handle negative values + if ( options.active < 0 ) { + options.active += this.headers.length; + } + this._refresh(); + }, + + _getCreateEventData: function() { + return { + header: this.active, + panel: !this.active.length ? $() : this.active.next(), + content: !this.active.length ? $() : this.active.next() + }; + }, + + _createIcons: function() { + var icons = this.options.icons; + if ( icons ) { + $( "" ) + .addClass( "ui-accordion-header-icon ui-icon " + icons.header ) + .prependTo( this.headers ); + this.active.children( ".ui-accordion-header-icon" ) + .removeClass( icons.header ) + .addClass( icons.activeHeader ); + this.headers.addClass( "ui-accordion-icons" ); + } + }, + + _destroyIcons: function() { + this.headers + .removeClass( "ui-accordion-icons" ) + .children( ".ui-accordion-header-icon" ) + .remove(); + }, + + _destroy: function() { + var contents; + + // clean up main element + this.element + .removeClass( "ui-accordion ui-widget ui-helper-reset" ) + .removeAttr( "role" ); + + // clean up headers + this.headers + .removeClass( "ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top" ) + .removeAttr( "role" ) + .removeAttr( "aria-selected" ) + .removeAttr( "aria-controls" ) + .removeAttr( "tabIndex" ) + .each(function() { + if ( /^ui-accordion/.test( this.id ) ) { + this.removeAttribute( "id" ); + } + }); + this._destroyIcons(); + + // clean up content panels + contents = this.headers.next() + .css( "display", "" ) + .removeAttr( "role" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-labelledby" ) + .removeClass( "ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled" ) + .each(function() { + if ( /^ui-accordion/.test( this.id ) ) { + this.removeAttribute( "id" ); + } + }); + if ( this.options.heightStyle !== "content" ) { + contents.css( "height", "" ); + } + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "event" ) { + if ( this.options.event ) { + this._off( this.headers, this.options.event ); + } + this._setupEvents( value ); + } + + this._super( key, value ); + + // setting collapsible: false while collapsed; open first panel + if ( key === "collapsible" && !value && this.options.active === false ) { + this._activate( 0 ); + } + + if ( key === "icons" ) { + this._destroyIcons(); + if ( value ) { + this._createIcons(); + } + } + + // #5332 - opacity doesn't cascade to positioned elements in IE + // so we need to add the disabled class to the headers and panels + if ( key === "disabled" ) { + this.headers.add( this.headers.next() ) + .toggleClass( "ui-state-disabled", !!value ); + } + }, + + _keydown: function( event ) { + /*jshint maxcomplexity:15*/ + if ( event.altKey || event.ctrlKey ) { + return; + } + + var keyCode = $.ui.keyCode, + length = this.headers.length, + currentIndex = this.headers.index( event.target ), + toFocus = false; + + switch ( event.keyCode ) { + case keyCode.RIGHT: + case keyCode.DOWN: + toFocus = this.headers[ ( currentIndex + 1 ) % length ]; + break; + case keyCode.LEFT: + case keyCode.UP: + toFocus = this.headers[ ( currentIndex - 1 + length ) % length ]; + break; + case keyCode.SPACE: + case keyCode.ENTER: + this._eventHandler( event ); + break; + case keyCode.HOME: + toFocus = this.headers[ 0 ]; + break; + case keyCode.END: + toFocus = this.headers[ length - 1 ]; + break; + } + + if ( toFocus ) { + $( event.target ).attr( "tabIndex", -1 ); + $( toFocus ).attr( "tabIndex", 0 ); + toFocus.focus(); + event.preventDefault(); + } + }, + + _panelKeyDown : function( event ) { + if ( event.keyCode === $.ui.keyCode.UP && event.ctrlKey ) { + $( event.currentTarget ).prev().focus(); + } + }, + + refresh: function() { + var options = this.options; + this._processPanels(); + + // was collapsed or no panel + if ( ( options.active === false && options.collapsible === true ) || !this.headers.length ) { + options.active = false; + this.active = $(); + // active false only when collapsible is true + } else if ( options.active === false ) { + this._activate( 0 ); + // was active, but active panel is gone + } else if ( this.active.length && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + // all remaining panel are disabled + if ( this.headers.length === this.headers.find(".ui-state-disabled").length ) { + options.active = false; + this.active = $(); + // activate previous panel + } else { + this._activate( Math.max( 0, options.active - 1 ) ); + } + // was active, active panel still exists + } else { + // make sure active index is correct + options.active = this.headers.index( this.active ); + } + + this._destroyIcons(); + + this._refresh(); + }, + + _processPanels: function() { + this.headers = this.element.find( this.options.header ) + .addClass( "ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" ); + + this.headers.next() + .addClass( "ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom" ) + .filter(":not(.ui-accordion-content-active)") + .hide(); + }, + + _refresh: function() { + var maxHeight, + options = this.options, + heightStyle = options.heightStyle, + parent = this.element.parent(), + accordionId = this.accordionId = "ui-accordion-" + + (this.element.attr( "id" ) || ++uid); + + this.active = this._findActive( options.active ) + .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ) + .removeClass( "ui-corner-all" ); + this.active.next() + .addClass( "ui-accordion-content-active" ) + .show(); + + this.headers + .attr( "role", "tab" ) + .each(function( i ) { + var header = $( this ), + headerId = header.attr( "id" ), + panel = header.next(), + panelId = panel.attr( "id" ); + if ( !headerId ) { + headerId = accordionId + "-header-" + i; + header.attr( "id", headerId ); + } + if ( !panelId ) { + panelId = accordionId + "-panel-" + i; + panel.attr( "id", panelId ); + } + header.attr( "aria-controls", panelId ); + panel.attr( "aria-labelledby", headerId ); + }) + .next() + .attr( "role", "tabpanel" ); + + this.headers + .not( this.active ) + .attr({ + "aria-selected": "false", + tabIndex: -1 + }) + .next() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }) + .hide(); + + // make sure at least one header is in the tab order + if ( !this.active.length ) { + this.headers.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active.attr({ + "aria-selected": "true", + tabIndex: 0 + }) + .next() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } + + this._createIcons(); + + this._setupEvents( options.event ); + + if ( heightStyle === "fill" ) { + maxHeight = parent.height(); + this.element.siblings( ":visible" ).each(function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + }); + + this.headers.each(function() { + maxHeight -= $( this ).outerHeight( true ); + }); + + this.headers.next() + .each(function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + }) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.headers.next() + .each(function() { + maxHeight = Math.max( maxHeight, $( this ).css( "height", "" ).height() ); + }) + .height( maxHeight ); + } + }, + + _activate: function( index ) { + var active = this._findActive( index )[ 0 ]; + + // trying to activate the already active panel + if ( active === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the currently active header + active = active || this.active[ 0 ]; + + this._eventHandler({ + target: active, + currentTarget: active, + preventDefault: $.noop + }); + }, + + _findActive: function( selector ) { + return typeof selector === "number" ? this.headers.eq( selector ) : $(); + }, + + _setupEvents: function( event ) { + var events = { + keydown: "_keydown" + }; + if ( event ) { + $.each( event.split(" "), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + }); + } + + this._off( this.headers.add( this.headers.next() ) ); + this._on( this.headers, events ); + this._on( this.headers.next(), { keydown: "_panelKeyDown" }); + this._hoverable( this.headers ); + this._focusable( this.headers ); + }, + + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + clicked = $( event.currentTarget ), + clickedIsActive = clicked[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : clicked.next(), + toHide = active.next(), + eventData = { + oldHeader: active, + oldPanel: toHide, + newHeader: collapsing ? $() : clicked, + newPanel: toShow + }; + + event.preventDefault(); + + if ( + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.headers.index( clicked ); + + // when the call to ._toggle() comes after the class changes + // it causes a very odd bug in IE 8 (see #6720) + this.active = clickedIsActive ? $() : clicked; + this._toggle( eventData ); + + // switch classes + // corner classes on the previously active header stay after the animation + active.removeClass( "ui-accordion-header-active ui-state-active" ); + if ( options.icons ) { + active.children( ".ui-accordion-header-icon" ) + .removeClass( options.icons.activeHeader ) + .addClass( options.icons.header ); + } + + if ( !clickedIsActive ) { + clicked + .removeClass( "ui-corner-all" ) + .addClass( "ui-accordion-header-active ui-state-active ui-corner-top" ); + if ( options.icons ) { + clicked.children( ".ui-accordion-header-icon" ) + .removeClass( options.icons.header ) + .addClass( options.icons.activeHeader ); + } + + clicked + .next() + .addClass( "ui-accordion-content-active" ); + } + }, + + _toggle: function( data ) { + var toShow = data.newPanel, + toHide = this.prevShow.length ? this.prevShow : data.oldPanel; + + // handle activating a panel during the animation for another activation + this.prevShow.add( this.prevHide ).stop( true, true ); + this.prevShow = toShow; + this.prevHide = toHide; + + if ( this.options.animate ) { + this._animate( toShow, toHide, data ); + } else { + toHide.hide(); + toShow.show(); + this._toggleComplete( data ); + } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + toHide.prev().attr( "aria-selected", "false" ); + // if we're switching panels, remove the old header from the tab order + // if we're opening from collapsed state, remove the previous header from the tab order + // if we're collapsing, then keep the collapsing header in the tab order + if ( toShow.length && toHide.length ) { + toHide.prev().attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.headers.filter(function() { + return $( this ).attr( "tabIndex" ) === 0; + }) + .attr( "tabIndex", -1 ); + } + + toShow + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }) + .prev() + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + }, + + _animate: function( toShow, toHide, data ) { + var total, easing, duration, + that = this, + adjust = 0, + down = toShow.length && + ( !toHide.length || ( toShow.index() < toHide.index() ) ), + animate = this.options.animate || {}, + options = down && animate.down || animate, + complete = function() { + that._toggleComplete( data ); + }; + + if ( typeof options === "number" ) { + duration = options; + } + if ( typeof options === "string" ) { + easing = options; + } + // fall back from options to animation in case of partial down settings + easing = easing || options.easing || animate.easing; + duration = duration || options.duration || animate.duration; + + if ( !toHide.length ) { + return toShow.animate( showProps, duration, easing, complete ); + } + if ( !toShow.length ) { + return toHide.animate( hideProps, duration, easing, complete ); + } + + total = toShow.show().outerHeight(); + toHide.animate( hideProps, { + duration: duration, + easing: easing, + step: function( now, fx ) { + fx.now = Math.round( now ); + } + }); + toShow + .hide() + .animate( showProps, { + duration: duration, + easing: easing, + complete: complete, + step: function( now, fx ) { + fx.now = Math.round( now ); + if ( fx.prop !== "height" ) { + adjust += fx.now; + } else if ( that.options.heightStyle !== "content" ) { + fx.now = Math.round( total - toHide.outerHeight() - adjust ); + adjust = 0; + } + } + }); + }, + + _toggleComplete: function( data ) { + var toHide = data.oldPanel; + + toHide + .removeClass( "ui-accordion-content-active" ) + .prev() + .removeClass( "ui-corner-top" ) + .addClass( "ui-corner-all" ); + + // Work around for rendering bug in IE (#5421) + if ( toHide.length ) { + toHide.parent()[0].className = toHide.parent()[0].className; + } + + this._trigger( "activate", null, data ); + } +}); + +})( jQuery ); diff --git a/extensions/jui/assets/jquery.ui.autocomplete.js b/extensions/jui/assets/jquery.ui.autocomplete.js new file mode 100644 index 0000000..ca53d2c --- /dev/null +++ b/extensions/jui/assets/jquery.ui.autocomplete.js @@ -0,0 +1,610 @@ +/*! + * jQuery UI Autocomplete 1.10.3 + * http://jqueryui.com + * + * Copyright 2013 jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/autocomplete/ + * + * Depends: + * jquery.ui.core.js + * jquery.ui.widget.js + * jquery.ui.position.js + * jquery.ui.menu.js + */ +(function( $, undefined ) { + +// used to prevent race conditions with remote data sources +var requestIndex = 0; + +$.widget( "ui.autocomplete", { + version: "1.10.3", + defaultElement: "", + options: { + appendTo: null, + autoFocus: false, + delay: 300, + minLength: 1, + position: { + my: "left top", + at: "left bottom", + collision: "none" + }, + source: null, + + // callbacks + change: null, + close: null, + focus: null, + open: null, + response: null, + search: null, + select: null + }, + + pending: 0, + + _create: function() { + // Some browsers only repeat keydown events, not keypress events, + // so we use the suppressKeyPress flag to determine if we've already + // handled the keydown event. #7269 + // Unfortunately the code for & in keypress is the same as the up arrow, + // so we use the suppressKeyPressRepeat flag to avoid handling keypress + // events when we know the keydown event was used to modify the + // search term. #7799 + var suppressKeyPress, suppressKeyPressRepeat, suppressInput, + nodeName = this.element[0].nodeName.toLowerCase(), + isTextarea = nodeName === "textarea", + isInput = nodeName === "input"; + + this.isMultiLine = + // Textareas are always multi-line + isTextarea ? true : + // Inputs are always single-line, even if inside a contentEditable element + // IE also treats inputs as contentEditable + isInput ? false : + // All other element types are determined by whether or not they're contentEditable + this.element.prop( "isContentEditable" ); + + this.valueMethod = this.element[ isTextarea || isInput ? "val" : "text" ]; + this.isNewMenu = true; + + this.element + .addClass( "ui-autocomplete-input" ) + .attr( "autocomplete", "off" ); + + this._on( this.element, { + keydown: function( event ) { + /*jshint maxcomplexity:15*/ + if ( this.element.prop( "readOnly" ) ) { + suppressKeyPress = true; + suppressInput = true; + suppressKeyPressRepeat = true; + return; + } + + suppressKeyPress = false; + suppressInput = false; + suppressKeyPressRepeat = false; + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + suppressKeyPress = true; + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + suppressKeyPress = true; + this._move( "nextPage", event ); + break; + case keyCode.UP: + suppressKeyPress = true; + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + suppressKeyPress = true; + this._keyEvent( "next", event ); + break; + case keyCode.ENTER: + case keyCode.NUMPAD_ENTER: + // when menu is open and has focus + if ( this.menu.active ) { + // #6055 - Opera still allows the keypress to occur + // which causes forms to submit + suppressKeyPress = true; + event.preventDefault(); + this.menu.select( event ); + } + break; + case keyCode.TAB: + if ( this.menu.active ) { + this.menu.select( event ); + } + break; + case keyCode.ESCAPE: + if ( this.menu.element.is( ":visible" ) ) { + this._value( this.term ); + this.close( event ); + // Different browsers have different default behavior for escape + // Single press can mean undo or clear + // Double press in IE means clear the whole form + event.preventDefault(); + } + break; + default: + suppressKeyPressRepeat = true; + // search timeout should be triggered before the input value is changed + this._searchTimeout( event ); + break; + } + }, + keypress: function( event ) { + if ( suppressKeyPress ) { + suppressKeyPress = false; + if ( !this.isMultiLine || this.menu.element.is( ":visible" ) ) { + event.preventDefault(); + } + return; + } + if ( suppressKeyPressRepeat ) { + return; + } + + // replicate some key handlers to allow them to repeat in Firefox and Opera + var keyCode = $.ui.keyCode; + switch( event.keyCode ) { + case keyCode.PAGE_UP: + this._move( "previousPage", event ); + break; + case keyCode.PAGE_DOWN: + this._move( "nextPage", event ); + break; + case keyCode.UP: + this._keyEvent( "previous", event ); + break; + case keyCode.DOWN: + this._keyEvent( "next", event ); + break; + } + }, + input: function( event ) { + if ( suppressInput ) { + suppressInput = false; + event.preventDefault(); + return; + } + this._searchTimeout( event ); + }, + focus: function() { + this.selectedItem = null; + this.previous = this._value(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + clearTimeout( this.searching ); + this.close( event ); + this._change( event ); + } + }); + + this._initSource(); + this.menu = $( "