diff --git a/apps/bootstrap/index.php b/apps/bootstrap/index.php index cf87d81..a0488ca 100644 --- a/apps/bootstrap/index.php +++ b/apps/bootstrap/index.php @@ -1,7 +1,4 @@ \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +msgctxt "context1" +msgid "" +"Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\n" +"aliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel " +"malesuada.\n" +"Nunc vel sapien nunc, a pretium nulla." +msgstr "" +"Олицетворение однократно. Представленный лексико-семантический анализ " +"является\n" +"психолингвистическим в своей основе, но механизм сочленений полидисперсен. " +"Впечатление\n" +"однократно. Различное расположение выбирает сюжетный механизм сочленений." + +msgctxt "context1" +msgid "String number two." +msgstr "Строка номер два." + +msgctxt "context2" +msgid "" +"The other\n" +"\n" +"context.\n" +msgstr "" +"Другой\n" +"\n" +"контекст.\n" + +msgctxt "context1" +msgid "" +"Missing\n" +"\r\t\"translation." +msgstr "" + +msgctxt "context1" +msgid "" +"Nunc vel sapien nunc, a pretium nulla.\n" +"Pellentesque habitant morbi tristique senectus et netus et malesuada fames " +"ac turpis egestas." +msgstr "Короткий перевод." + +msgid "contextless" +msgstr "" + +msgctxt "context2" +msgid "" +"test1\\ntest2\n" +"\\\n" +"test3" +msgstr "" +"тест1\\nтест2\n" +"\\\n" +"тест3" diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php new file mode 100644 index 0000000..7b499f4 --- /dev/null +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -0,0 +1,14 @@ +markTestSkipped(); + } +} diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php new file mode 100644 index 0000000..0aa22da --- /dev/null +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -0,0 +1,95 @@ +load($moFilePath, 'context1'); + $context2 = $moFile->load($moFilePath, 'context2'); + + // item count + $this->assertCount(3, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2); + + // translated messages + $this->assertFalse(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.mo')) { + unlink($poFileDirectory . '/test.mo'); + } + + $moFile = new GettextMoFile(); + $moFile->save($poFileDirectory . '/test.mo', $messages); + + // load messages + $context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1'); + $context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php new file mode 100644 index 0000000..8dddb40 --- /dev/null +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -0,0 +1,95 @@ +load($poFilePath, 'context1'); + $context2 = $poFile->load($poFilePath, 'context2'); + + // item count + $this->assertCount(4, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2); + + // translated messages + $this->assertTrue(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.po')) { + unlink($poFileDirectory . '/test.po'); + } + + $poFile = new GettextPoFile(); + $poFile->save($poFileDirectory . '/test.po', $messages); + + // load messages + $context1 = $poFile->load($poFileDirectory . '/test.po', 'context1'); + $context2 = $poFile->load($poFileDirectory . '/test.po', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/yii/base/Application.php b/yii/base/Application.php index 2e068ef..fb9a6c1 100644 --- a/yii/base/Application.php +++ b/yii/base/Application.php @@ -84,6 +84,13 @@ class Application extends Module } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } + + if (isset($config['timeZone'])) { + $this->setTimeZone($config['timeZone']); + unset($config['timeZone']); + } elseif (!ini_get('date.timezone')) { + $this->setTimeZone('UTC'); + } $this->registerErrorHandlers(); $this->registerCoreComponents(); @@ -222,6 +229,8 @@ class Application extends Module /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). + * If time zone is not configured in php.ini or application config, + * it will be set to UTC by default. * @return string the time zone used by this application. * @see http://php.net/manual/en/function.date-default-timezone-get.php */ diff --git a/yii/i18n/GettextFile.php b/yii/i18n/GettextFile.php new file mode 100644 index 0000000..03eecca --- /dev/null +++ b/yii/i18n/GettextFile.php @@ -0,0 +1,37 @@ + + * @since 2.0 + */ +abstract class GettextFile extends Component +{ + /** + * Loads messages from a file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + abstract public function load($filePath, $context); + + /** + * Saves messages to a file. + * @param string $filePath file path + * @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. + */ + abstract public function save($filePath, $messages); +} diff --git a/yii/i18n/GettextMessageSource.php b/yii/i18n/GettextMessageSource.php new file mode 100644 index 0000000..0eb7cb3 --- /dev/null +++ b/yii/i18n/GettextMessageSource.php @@ -0,0 +1,59 @@ +basePath) . '/' . $language . '/' . $this->catalog; + if ($this->useMoFile) { + $messageFile .= static::MO_FILE_EXT; + } else { + $messageFile .= static::PO_FILE_EXT; + } + + if (is_file($messageFile)) { + if ($this->useMoFile) { + $gettextFile = new GettextMoFile(array('useBigEndian' => $this->useBigEndian)); + } else { + $gettextFile = new GettextPoFile(); + } + $messages = $gettextFile->load($messageFile, $category); + if (!is_array($messages)) { + $messages = array(); + } + return $messages; + } else { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); + return array(); + } + } +} diff --git a/yii/i18n/GettextMoFile.php b/yii/i18n/GettextMoFile.php new file mode 100644 index 0000000..bacba52 --- /dev/null +++ b/yii/i18n/GettextMoFile.php @@ -0,0 +1,267 @@ +. + * 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. + * + * 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. + * + * @author Qiang Xue + * @since 2.0 + */ +class GettextMoFile extends GettextFile +{ + /** + * @var boolean whether to use big-endian when reading and writing an integer. + */ + public $useBigEndian = false; + + /** + * Loads messages from an MO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + if (false === ($fileHandle = @fopen($filePath, 'rb'))) { + throw new Exception('Unable to read file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_SH)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + $array = unpack('c', $this->readBytes($fileHandle, 4)); + $magic = current($array); + if ($magic == -34) { + $this->useBigEndian = false; + } elseif ($magic == -107) { + $this->useBigEndian = true; + } else { + throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').'); + } + + // revision + $revision = $this->readInteger($fileHandle); + if ($revision != 0) { + throw new Exception('Invalid MO file revision: ' . $revision . '.'); + } + + $count = $this->readInteger($fileHandle); + $sourceOffset = $this->readInteger($fileHandle); + $targetOffset = $this->readInteger($fileHandle); + + $sourceLengths = array(); + $sourceOffsets = array(); + fseek($fileHandle, $sourceOffset); + for ($i = 0; $i < $count; ++$i) { + $sourceLengths[] = $this->readInteger($fileHandle); + $sourceOffsets[] = $this->readInteger($fileHandle); + } + + $targetLengths = array(); + $targetOffsets = array(); + fseek($fileHandle, $targetOffset); + for ($i = 0; $i < $count; ++$i) { + $targetLengths[] = $this->readInteger($fileHandle); + $targetOffsets[] = $this->readInteger($fileHandle); + } + + $messages = array(); + for ($i = 0; $i < $count; ++$i) { + $id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]); + $separatorPosition = strpos($id, chr(4)); + + if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || + (!$context && $separatorPosition === false)) { + if ($separatorPosition !== false) { + $id = substr($id,$separatorPosition+1); + } + + $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); + $messages[$id] = $message; + } + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + return $messages; + } + + /** + * Saves messages to an MO file. + * @param string $filePath file path + * @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. + */ + public function save($filePath, $messages) + { + if (false === ($fileHandle = @fopen($filePath, 'wb'))) { + throw new Exception('Unable to write file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_EX)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + if ($this->useBigEndian) { + $this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107 + } else { + $this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34 + } + + // revision + $this->writeInteger($fileHandle, 0); + + // message count + $messageCount = count($messages); + $this->writeInteger($fileHandle, $messageCount); + + // offset of source message table + $offset = 28; + $this->writeInteger($fileHandle, $offset); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // hashtable size, omitted + $this->writeInteger($fileHandle, 0); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // length and offsets for source messages + foreach (array_keys($messages) as $id) { + $length = strlen($id); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // length and offsets for target messages + foreach ($messages as $message) { + $length = strlen($message); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // source messages + foreach (array_keys($messages) as $id) { + $this->writeString($fileHandle, $id); + } + + // target messages + foreach ($messages as $message) { + $this->writeString($fileHandle, $message); + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + } + + /** + * Reads one or several bytes. + * @param resource $fileHandle to read from + * @param integer $byteCount to be read + * @return string bytes + */ + protected function readBytes($fileHandle, $byteCount = 1) + { + if ($byteCount > 0) { + return fread($fileHandle, $byteCount); + } + } + + /** + * Write bytes. + * @param resource $fileHandle to write to + * @param string $bytes to be written + * @return integer how many bytes are written + */ + protected function writeBytes($fileHandle, $bytes) + { + return fwrite($fileHandle, $bytes); + } + + /** + * Reads a 4-byte integer. + * @param resource $fileHandle to read from + * @return integer the result + */ + protected function readInteger($fileHandle) + { + $array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4)); + return current($array); + } + + /** + * Writes a 4-byte integer. + * @param resource $fileHandle to write to + * @param integer $integer to be written + * @return integer how many bytes are written + */ + protected function writeInteger($fileHandle, $integer) + { + return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int)$integer)); + } + + /** + * Reads a string. + * @param resource $fileHandle file handle + * @param integer $length of the string + * @param integer $offset of the string in the file. If null, it reads from the current position. + * @return string the result + */ + protected function readString($fileHandle, $length, $offset = null) + { + if ($offset !== null) { + fseek($fileHandle, $offset); + } + return $this->readBytes($fileHandle, $length); + } + + /** + * Writes a string. + * @param resource $fileHandle to write to + * @param string $string to be written + * @return integer how many bytes are written + */ + protected function writeString($fileHandle, $string) + { + return $this->writeBytes($fileHandle, $string. "\0"); + } +} diff --git a/yii/i18n/GettextPoFile.php b/yii/i18n/GettextPoFile.php new file mode 100644 index 0000000..cac075b --- /dev/null +++ b/yii/i18n/GettextPoFile.php @@ -0,0 +1,97 @@ + + * @since 2.0 + */ +class GettextPoFile extends GettextFile +{ + /** + * Loads messages from a PO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + $pattern = '/(msgctxt\s+"(.*?(?decode($matches[3][$i]); + $message = $this->decode($matches[4][$i]); + $messages[$id] = $message; + } + } + return $messages; + } + + /** + * Saves messages to a PO file. + * @param string $filePath file path + * @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. + */ + public function save($filePath, $messages) + { + $content = ''; + foreach ($messages as $id => $message) { + $separatorPosition = strpos($id, chr(4)); + if ($separatorPosition !== false) { + $content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n"; + $id = substr($id, $separatorPosition + 1); + } + $content .= 'msgid "' . $this->encode($id) . "\"\n"; + $content .= 'msgstr "' . $this->encode($message) . "\"\n\n"; + } + file_put_contents($filePath, $content); + } + + /** + * Encodes special characters in a message. + * @param string $string message to be encoded + * @return string the encoded message + */ + protected function encode($string) + { + return str_replace( + array('"', "\n", "\t", "\r"), + array('\\"', '\\n', '\\t', '\\r'), + $string + ); + } + + /** + * Decodes special characters in a message. + * @param string $string message to be decoded + * @return string the decoded message + */ + protected function decode($string) + { + $string = preg_replace( + array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'), + array('', "\n", "\r", "\t", '"'), + $string + ); + return substr(rtrim($string), 1, -1); + } +} diff --git a/yii/web/Pagination.php b/yii/web/Pagination.php index 4aeee78..8d08c93 100644 --- a/yii/web/Pagination.php +++ b/yii/web/Pagination.php @@ -73,7 +73,7 @@ class Pagination extends \yii\base\Object * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. * If false and [[page]] is 0, the page parameter will not be put in the URL. */ - public $forcePageVar = false; + public $forcePageVar = true; /** * @var string the route of the controller action for displaying the paged contents. * If not set, it means using the currently requested route. diff --git a/yii/widgets/LinkPager.php b/yii/widgets/LinkPager.php new file mode 100644 index 0000000..1651246 --- /dev/null +++ b/yii/widgets/LinkPager.php @@ -0,0 +1,201 @@ + + * @since 2.0 + */ +class LinkPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the pager container tag. + */ + public $options = array('class' => 'pagination'); + /** + * @var string the CSS class for the "first" page button. + */ + public $firstPageCssClass = 'first'; + /** + * @var string the CSS class for the "last" page button. + */ + public $lastPageCssClass = 'last'; + /** + * @var string the CSS class for the "previous" page button. + */ + public $prevPageCssClass = 'prev'; + /** + * @var string the CSS class for the "next" page button. + */ + public $nextPageCssClass = 'next'; + /** + * @var string the CSS class for the active (currently selected) page button. + */ + public $activePageCssClass = 'active'; + /** + * @var string the CSS class for the disabled page buttons. + */ + public $disabledPageCssClass = 'disabled'; + /** + * @var integer maximum number of page buttons that can be displayed. Defaults to 10. + */ + public $maxButtonCount = 10; + /** + * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "next" page button will not be displayed. + */ + public $nextPageLabel = '»'; + /** + * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "previous" page button will not be displayed. + */ + public $prevPageLabel = '«'; + /** + * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "first" page button will not be displayed. + */ + public $firstPageLabel; + /** + * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "last" page button will not be displayed. + */ + public $lastPageLabel; + /** + * @var string the template used to render the content within the pager container. + * The token "{buttons}" will be replaced with the actual page buttons. + */ + public $template = '{buttons}'; + + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + $buttons = strtr($this->template, array( + '{buttons}' => Html::tag('ul', implode("\n", $this->createPageButtons())), + )); + echo Html::tag('div', $buttons, $this->options); + } + + /** + * Creates the page buttons. + * @return array a list of page buttons (in HTML code). + */ + protected function createPageButtons() + { + $buttons = array(); + + $pageCount = $this->pagination->pageCount; + $currentPage = $this->pagination->getPage(); + + // first page + if ($this->firstPageLabel !== null) { + $buttons[] = $this->createPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); + } + + // prev page + if ($this->prevPageLabel !== null) { + if (($page = $currentPage - 1) < 0) { + $page = 0; + } + $buttons[] = $this->createPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); + } + + // internal pages + list($beginPage, $endPage) = $this->getPageRange(); + for ($i = $beginPage; $i <= $endPage; ++$i) { + $buttons[] = $this->createPageButton($i + 1, $i, null, false, $i == $currentPage); + } + + // next page + if ($this->nextPageLabel !== null) { + if (($page = $currentPage + 1) >= $pageCount - 1) { + $page = $pageCount - 1; + } + $buttons[] = $this->createPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); + } + + // last page + if ($this->lastPageLabel !== null) { + $buttons[] = $this->createPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); + } + + return $buttons; + } + + /** + * Creates a page button. + * You may override this method to customize the generation of page buttons. + * @param string $label the text label for the button + * @param integer $page the page number + * @param string $class the CSS class for the page button. + * @param boolean $disabled whether this page button is disabled + * @param boolean $active whether this page button is active + * @return string the generated button + */ + protected function createPageButton($label, $page, $class, $disabled, $active) + { + if ($active) { + $class .= ' ' . $this->activePageCssClass; + } + if ($disabled) { + $class .= ' ' . $this->disabledPageCssClass; + } + $class = trim($class); + $options = array('class' => $class === '' ? null : $class); + return Html::tag('li', Html::a($label, $this->pagination->createUrl($page)), $options); + } + + /** + * @return array the begin and end pages that need to be displayed. + */ + protected function getPageRange() + { + $currentPage = $this->pagination->getPage(); + $pageCount = $this->pagination->getPageCount(); + + $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); + if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { + $endPage = $pageCount - 1; + $beginPage = max(0, $endPage - $this->maxButtonCount + 1); + } + return array($beginPage, $endPage); + } +} \ No newline at end of file diff --git a/yii/widgets/ListPager.php b/yii/widgets/ListPager.php new file mode 100644 index 0000000..1bc2b21 --- /dev/null +++ b/yii/widgets/ListPager.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +class ListPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make ListPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the drop-down list tag. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + */ + public $options = array(); + /** + * @var string the template used to render the label for each list option. + * The token "{page}" will be replaced with the actual page number (1-based). + */ + public $template = '{page}'; + + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + $pageCount = $this->pagination->pageCount; + $currentPage = $this->pagination->getPage(); + + $pages = array(); + for ($i = 0; $i < $pageCount; ++$i) { + $pages[$this->pagination->createUrl($i)] = $this->generatePageText($i); + } + $selection = $this->pagination->createUrl($currentPage); + + if (!isset($this->options['onchange'])) { + $this->options['onchange'] = "if(this.value!='') {window.location=this.value;};"; + } + + echo Html::dropDownList(null, $selection, $pages, $this->options); + } + + /** + * Generates the label of the list option for the specified page number. + * You may override this method to customize the option display. + * @param integer $page zero-based page number + * @return string the list option for the page number + */ + protected function generatePageText($page) + { + return strtr($this->template, array( + '{page}' => $page + 1, + )); + } + +} \ No newline at end of file diff --git a/yii/yiic.php b/yii/yiic.php index 3803455..7cd0c40 100644 --- a/yii/yiic.php +++ b/yii/yiic.php @@ -7,10 +7,6 @@ * @license http://www.yiiframework.com/license/ */ -if (!ini_get('date.timezone')) { - date_default_timezone_set('UTC'); -} - defined('YII_DEBUG') or define('YII_DEBUG', true); // fcgi doesn't have STDIN defined by default