From d67c43ad7d166a911e10e97b88fe353957a0d797 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Fri, 15 Oct 2010 03:48:39 +0000 Subject: [PATCH] ENHANCEMENT Using jQuery UI datepicker in DateField and DatetimeField instead of outdated DHTML calendar.js (fixes #5397) ENHANCEMENT Abstracted optional DateField->setConfig('showcalendar') logic to DateField_View_JQuery (from r107438) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@112597 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- css/DateField.css | 20 --- css/DatetimeField.css | 25 +-- forms/DateField.php | 214 +++++++++++++++++++----- javascript/DateField.js | 14 ++ tests/forms/DatefieldViewJQueryTest.php | 24 +++ tests/forms/DatetimeFieldTest.php | 46 +++++ 6 files changed, 266 insertions(+), 77 deletions(-) delete mode 100755 css/DateField.css create mode 100644 javascript/DateField.js create mode 100644 tests/forms/DatefieldViewJQueryTest.php diff --git a/css/DateField.css b/css/DateField.css deleted file mode 100755 index c44b2a25b..000000000 --- a/css/DateField.css +++ /dev/null @@ -1,20 +0,0 @@ -.calendardate .calendar table { - width: 200px; -} -.calendardate img { - position: relative; - top: 2px; - cursor: pointer; -} - -.calendarpopup { - position: absolute; - left: 0em; - top: 2em; - display: none; - z-index: 2; -} - -.calendarpopup.focused { - display: block; -} diff --git a/css/DatetimeField.css b/css/DatetimeField.css index 0865194c9..3b3c8c100 100644 --- a/css/DatetimeField.css +++ b/css/DatetimeField.css @@ -1,21 +1,14 @@ -.popupdatetime ul { - list-style:none; - padding-left:0; - font-size:1em; +.datetime .middleColumn .middleColumn { + margin: 0; + padding: 0; + clear: none; + float: left; } -.popupdatetime ul li { - display:inline; +.datetime .date .middleColumn { + width: 20em; } -.popupdatetime ul li .calendarpopup { - top: 2em; - left: -1px; - - padding-top: 30px; -} - -.popupdatetime ul li .dropdownpopup { - top: 2em; - left: -1px; +.datetime .time .middleColumn { + width: 10em; } \ No newline at end of file diff --git a/forms/DateField.php b/forms/DateField.php index cabd8a29e..123e60077 100755 --- a/forms/DateField.php +++ b/forms/DateField.php @@ -9,13 +9,13 @@ require_once 'Zend/Date.php'; * # Configuration * * - 'showcalendar' (boolean): Determines if a calendar picker is shown. - * By default, "DHTML Calendar" is used,see http://www.dynarch.com/projects/calendar. - * CAUTION: Only works in NZ date format, see calendar-setup.js + * By default, jQuery UI datepicker is used (see {@link DateField_View_JQuery}). + * - 'jslocale' (string): Overwrites the "Locale" value set in this class. + * Only useful in combination with {@link DateField_View_JQuery}. * - 'dmyfields' (boolean): Show three input fields for day, month and year separately. * CAUTION: Might not be useable in combination with 'showcalendar', depending on the used javascript library * - 'dateformat' (string): Date format compatible with Zend_Date. - * Usually set to default format for {@link locale} - * through {@link Zend_Locale_Format::getDateFormat()}. + * Usually set to default format for {@link locale} through {@link Zend_Locale_Format::getDateFormat()}. * - 'datavalueformat' (string): Internal ISO format string used by {@link dataValue()} to save the * date to a database. * - 'min' (string): Minimum allowed date value (in ISO format, or strtotime() compatible). @@ -52,6 +52,7 @@ class DateField extends TextField { */ protected $config = array( 'showcalendar' => false, + 'jslocale' => null, 'dmyfields' => false, 'dmyseparator' => ' / ', 'dateformat' => null, @@ -85,7 +86,13 @@ class DateField extends TextField { } function FieldHolder() { - return parent::FieldHolder(); + // TODO Replace with properly extensible view helper system + $d = Object::create('DateField_View_JQuery', $this); + $d->onBeforeRender(); + $html = parent::FieldHolder(); + $html = $d->onAfterRender($html); + + return $html; } function Field() { @@ -120,42 +127,9 @@ class DateField extends TextField { $html = parent::Field(); } - $html = $this->FieldDriver($html); - - // wrap in additional div for legacy reasons and to apply behaviour correctly - if($this->getConfig('showcalendar')) $html = sprintf('
%s
', $html); - return $html; } - - /** - * Caution: API might change. This will evolve into a pluggable - * API for 'form field drivers' which can add their own - * markup and requirements. - * - * @param String $html - * @return $html - */ - protected function FieldDriver($html) { - // Optionally add a "DHTML" calendar icon. Mainly legacy, a date picker - // should be unobtrusively added by javascript (e.g. jQuery UI). - // CAUTION: Only works in NZ date format, see calendar-setup.js - if($this->getConfig('showcalendar')) { - Requirements::javascript(THIRDPARTY_DIR . '/prototype/prototype.js'); - Requirements::javascript(THIRDPARTY_DIR . '/behaviour/behaviour.js'); - Requirements::javascript(THIRDPARTY_DIR . "/calendar/calendar.js"); - Requirements::javascript(THIRDPARTY_DIR . "/calendar/lang/calendar-en.js"); - Requirements::javascript(THIRDPARTY_DIR . "/calendar/calendar-setup.js"); - Requirements::css(SAPPHIRE_DIR . "/css/DateField.css"); - Requirements::css(THIRDPARTY_DIR . "/calendar/calendar-win2k-1.css"); - - $html .= sprintf('Calendar icon', $this->id()); - $html .= sprintf('
', $this->id()); - } - return $html; - } - /** * Sets the internal value to ISO date format. * @@ -442,9 +416,6 @@ JS; throw new InvalidArgumentException('Date "%s" is not a valid maximum date format (%s) or strtotime() argument', $val, $format); } break; - case 'showcalendar': - $this->config['dateformat'] = Zend_Locale_Format::getDateFormat('en_NZ'); - break; } $this->config[$name] = $val; @@ -498,4 +469,165 @@ class DateField_Disabled extends DateField { } } +/** + * Preliminary API to separate optional view properties + * like calendar popups from the actual datefield logic. + * + * Caution: This API is highly volatile, and might change without prior deprecation. + * + * @package sapphire + * @subpackage forms + */ +class DateField_View_JQuery { + + protected $field; + + /** + * @var array Maps values from {@link i18n::$all_locales()} to + * localizations existing in jQuery UI. + */ + static $locale_map = array( + 'en_GB' => 'en-GB', + 'fr_CH' => 'fr-CH', + 'pt_BR' => 'pt-BR', + 'sr_SR' => 'sr-SR', + 'zh_CN' => 'zh-CN', + 'zh_HK' => 'zh-HK', + 'zh_TW' => 'zh-TW', + ); + + /** + * @param DateField $field + */ + function __construct($field) { + $this->field = $field; + } + + /** + * @return DateField + */ + function getField() { + return $this->field; + } + + /** + * + */ + function onBeforeRender() { + if($this->getField()->getConfig('showcalendar')) { + // Inject configuration into existing HTML + $format = self::convert_iso_to_jquery_format($this->getField()->getConfig('dateformat')); + $this->getField()->addExtraClass(str_replace('"', '\'', Convert::raw2json(array('dateFormat' => $format)))); + } + } + + /** + * @param String $html + * @return + */ + function onAfterRender($html) { + if($this->getField()->getConfig('showcalendar')) { + Requirements::javascript(THIRDPARTY_DIR . '/jquery/jquery.js'); + Requirements::javascript(SAPPHIRE_DIR . '/javascript/jquery_improvements.js'); + Requirements::css('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.1/themes/smoothness/jquery-ui.css'); + Requirements::javascript('http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.1/jquery-ui.min.js'); + + // Include language files (if required) + $lang = $this->getLang(); + if($lang != 'en') { + // TODO Check for existence of locale to avoid unnecessary 404s from the CDN + Requirements::javascript( + sprintf( + 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.1/i18n/jquery.ui.datepicker-%s.min.js', + // can be a mix between names (e.g. 'de') and combined locales (e.g. 'zh-TW') + $lang + )); + } + + Requirements::javascript(THIRDPARTY_DIR . "/jquery-metadata/jquery.metadata.js"); + Requirements::javascript(SAPPHIRE_DIR . "/javascript/DateField.js"); + } + + return $html; + } + + /** + * Determines which language to use for jQuery UI, which + * can be different from the value set in i18n. + * + * @return String + */ + protected function getLang() { + $locale = $this->getField()->getLocale(); + if($this->getField()->getConfig('jslocale')) { + // Undocumented config property for now, might move to the jQuery view helper + $lang = $this->getField()->getConfig('jslocale'); + } else if(array_key_exists($locale, self::$locale_map)) { + // Specialized mapping for combined lang properties + $lang = self::$locale_map[$locale]; + } else { + // Fall back to default lang (meaning "en_US" turns into "en") + $lang = i18n::get_lang_from_locale($locale); + } + + return $lang; + } + + /** + * Convert iso to jquery UI date format. + * Needs to be consistent with Zend formatting, otherwise validation will fail. + * Removes all time settings like hour/minute/second from the format. + * See http://docs.jquery.com/UI/Datepicker/formatDate + * + * @param String $format + * @return String + */ + static function convert_iso_to_jquery_format($format) { + $convert = array( + '/([^d])d([^d])/' => '$1d$2', + '/^d([^d])/' => 'd$1', + '/([^d])d$/' => '$1d', + '/dd/' => 'dd', + '/EEEE/' => 'DD', + '/EEE/' => 'D', + '/SS/' => '', + '/eee/' => 'd', + '/e/' => 'N', + '/D/' => '', + '/w/' => '', + '/([^M])M([^M])/' => '$1m$2', + '/^M([^M])/' => 'm$1', + '/([^M])M$/' => '$1m', + '/MMMM/' => 'MM', + '/MMM/' => 'M', + '/MM/' => 'mm', + '/l/' => '', + '/YYYY/' => 'yy', + '/yyyy/' => 'yy', + '/[^y]yy[^y]/' => 'y', + '/a/' => '', + '/B/' => '', + '/hh/' => '', + '/h/' => '', + '/([^H])H([^H])/' => '', + '/^H([^H])/' => '', + '/([^H])H$/' => '', + '/HH/' => '', + // '/mm/' => '', + '/ss/' => '', + '/zzzz/' => '', + '/I/' => '', + '/ZZZZ/' => '', + '/Z/' => '', + '/z/' => '', + '/X/' => '', + '/r/' => '', + '/U/' => '', + ); + $patterns = array_keys($convert); + $replacements = array_values($convert); + + return preg_replace($patterns, $replacements, $format); + } +} ?> \ No newline at end of file diff --git a/javascript/DateField.js b/javascript/DateField.js new file mode 100644 index 000000000..05ae32f6c --- /dev/null +++ b/javascript/DateField.js @@ -0,0 +1,14 @@ +(function($) { + $('.field.date input.text').live('click', function() { + var holder = $(this).parents('.field.date:first'), config = holder.metadata(); + + if(config.locale && $.datepicker.regional[config.locale]) { + config = $.extend(config, $.datepicker.regional[config.locale], {}); + } + + // Initialize and open a datepicker + // live() doesn't have "onmatch", and jQuery.entwine is a bit too heavyweight for this, so we need to do this onclick. + $(this).datepicker(config); + $(this).datepicker('show'); + }); +}(jQuery)); \ No newline at end of file diff --git a/tests/forms/DatefieldViewJQueryTest.php b/tests/forms/DatefieldViewJQueryTest.php new file mode 100644 index 000000000..f0b0cd645 --- /dev/null +++ b/tests/forms/DatefieldViewJQueryTest.php @@ -0,0 +1,24 @@ +assertEquals( + 'M d, yy', + DateField_View_JQuery::convert_iso_to_jquery_format('MMM d, yyyy') + ); + + $this->assertEquals( + 'd/mm/yy', + DateField_View_JQuery::convert_iso_to_jquery_format('d/MM/yyyy') + ); + + $this->assertEquals( + 'dd.mm.yy', + DateField_View_JQuery::convert_iso_to_jquery_format('dd.MM.yyyy') + ); + } +} \ No newline at end of file diff --git a/tests/forms/DatetimeFieldTest.php b/tests/forms/DatetimeFieldTest.php index 31adbf411..6456c7921 100644 --- a/tests/forms/DatetimeFieldTest.php +++ b/tests/forms/DatetimeFieldTest.php @@ -18,6 +18,40 @@ class DatetimeFieldTest extends SapphireTest { i18n::set_locale($this->originalLocale); } + function testFormSaveInto() { + $form = new Form( + new Controller(), + 'Form', + new FieldSet( + $f = new DatetimeField('MyDatetime', null) + ), + new FieldSet( + new FormAction('doSubmit') + ) + ); + $f->setValue(array( + 'date' => '29/03/2003', + 'time' => '23:59:38' + )); + $m = new DatetimeFieldTest_Model(); + $form->saveInto($m); + $this->assertEquals('2003-03-29 23:59:38', $m->MyDatetime); + } + + function testDataValue() { + $f = new DatetimeField('Datetime'); + $this->assertEquals(null, $f->dataValue(), 'Empty field'); + + $f = new DatetimeField('Datetime', null, '2003-03-29 23:59:38'); + $this->assertEquals('2003-03-29 23:59:38', $f->dataValue(), 'From date/time string'); + + $f = new DatetimeField('Datetime', null, '2003-03-29'); + $this->assertEquals('2003-03-29 00:00:00', $f->dataValue(), 'From date string (no time)'); + + $f = new DatetimeField('Datetime', null, array('date' => '2003-03-29', 'time' => null)); + $this->assertEquals('2003-03-29 00:00:00', $f->dataValue(), 'From date array (no time)'); + } + function testConstructorWithoutArgs() { $f = new DatetimeField('Datetime'); $this->assertEquals($f->dataValue(), null); @@ -77,4 +111,16 @@ class DatetimeFieldTest extends SapphireTest { $f = new DatetimeField('Datetime', 'Datetime', 'wrong'); $this->assertFalse($f->validate(new RequiredFields())); } +} + +/** + * @package sapphire + * @subpackage tests + */ +class DatetimeFieldTest_Model extends DataObject implements TestOnly { + + static $db = array( + 'MyDatetime' => 'SS_Datetime' + ); + } \ No newline at end of file