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
This commit is contained in:
Ingo Schommer 2010-10-15 03:48:39 +00:00
parent ab918e8546
commit d67c43ad7d
6 changed files with 266 additions and 77 deletions

View File

@ -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;
}

View File

@ -1,21 +1,14 @@
.popupdatetime ul { .datetime .middleColumn .middleColumn {
list-style:none; margin: 0;
padding-left:0; padding: 0;
font-size:1em; clear: none;
float: left;
} }
.popupdatetime ul li { .datetime .date .middleColumn {
display:inline; width: 20em;
} }
.popupdatetime ul li .calendarpopup { .datetime .time .middleColumn {
top: 2em; width: 10em;
left: -1px;
padding-top: 30px;
}
.popupdatetime ul li .dropdownpopup {
top: 2em;
left: -1px;
} }

View File

@ -9,13 +9,13 @@ require_once 'Zend/Date.php';
* # Configuration * # Configuration
* *
* - 'showcalendar' (boolean): Determines if a calendar picker is shown. * - 'showcalendar' (boolean): Determines if a calendar picker is shown.
* By default, "DHTML Calendar" is used,see http://www.dynarch.com/projects/calendar. * By default, jQuery UI datepicker is used (see {@link DateField_View_JQuery}).
* CAUTION: Only works in NZ date format, see calendar-setup.js * - '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. * - '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 * CAUTION: Might not be useable in combination with 'showcalendar', depending on the used javascript library
* - 'dateformat' (string): Date format compatible with Zend_Date. * - 'dateformat' (string): Date format compatible with Zend_Date.
* Usually set to default format for {@link locale} * Usually set to default format for {@link locale} through {@link Zend_Locale_Format::getDateFormat()}.
* through {@link Zend_Locale_Format::getDateFormat()}.
* - 'datavalueformat' (string): Internal ISO format string used by {@link dataValue()} to save the * - 'datavalueformat' (string): Internal ISO format string used by {@link dataValue()} to save the
* date to a database. * date to a database.
* - 'min' (string): Minimum allowed date value (in ISO format, or strtotime() compatible). * - 'min' (string): Minimum allowed date value (in ISO format, or strtotime() compatible).
@ -52,6 +52,7 @@ class DateField extends TextField {
*/ */
protected $config = array( protected $config = array(
'showcalendar' => false, 'showcalendar' => false,
'jslocale' => null,
'dmyfields' => false, 'dmyfields' => false,
'dmyseparator' => '&nbsp;<span class="separator">/</span>&nbsp;', 'dmyseparator' => '&nbsp;<span class="separator">/</span>&nbsp;',
'dateformat' => null, 'dateformat' => null,
@ -85,7 +86,13 @@ class DateField extends TextField {
} }
function FieldHolder() { 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() { function Field() {
@ -120,39 +127,6 @@ class DateField extends TextField {
$html = parent::Field(); $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('<div class="calendardate">%s</div>', $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('<img src="sapphire/images/calendar-icon.gif" id="%s-icon" alt="Calendar icon" />', $this->id());
$html .= sprintf('<div class="calendarpopup" id="%s-calendar"></div>', $this->id());
}
return $html; return $html;
} }
@ -442,9 +416,6 @@ JS;
throw new InvalidArgumentException('Date "%s" is not a valid maximum date format (%s) or strtotime() argument', $val, $format); throw new InvalidArgumentException('Date "%s" is not a valid maximum date format (%s) or strtotime() argument', $val, $format);
} }
break; break;
case 'showcalendar':
$this->config['dateformat'] = Zend_Locale_Format::getDateFormat('en_NZ');
break;
} }
$this->config[$name] = $val; $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);
}
}
?> ?>

14
javascript/DateField.js Normal file
View File

@ -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));

View File

@ -0,0 +1,24 @@
<?php
/**
* @package sapphire
* @subpackage tests
*/
class DateFieldViewJQueryTest extends SapphireTest {
function testConvert() {
$this->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')
);
}
}

View File

@ -18,6 +18,40 @@ class DatetimeFieldTest extends SapphireTest {
i18n::set_locale($this->originalLocale); 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() { function testConstructorWithoutArgs() {
$f = new DatetimeField('Datetime'); $f = new DatetimeField('Datetime');
$this->assertEquals($f->dataValue(), null); $this->assertEquals($f->dataValue(), null);
@ -78,3 +112,15 @@ class DatetimeFieldTest extends SapphireTest {
$this->assertFalse($f->validate(new RequiredFields())); $this->assertFalse($f->validate(new RequiredFields()));
} }
} }
/**
* @package sapphire
* @subpackage tests
*/
class DatetimeFieldTest_Model extends DataObject implements TestOnly {
static $db = array(
'MyDatetime' => 'SS_Datetime'
);
}