API HTML5 date/time fields, remove member prefs (fixes #6626)

This commit is contained in:
Ingo Schommer 2017-03-31 10:37:21 +13:00
parent ac6d4f3038
commit 326aa37ea4
16 changed files with 209 additions and 703 deletions

View File

@ -40,19 +40,19 @@ A custom date format for a [api:DateField] can be provided through `setDateForma
DateField::create('MyDate')->setDateFormat('dd-MM-yyyy'); DateField::create('MyDate')->setDateFormat('dd-MM-yyyy');
<div class="info" markdown="1"> <div class="info" markdown="1">
The formats are based on [CLDR format](http://userguide.icu-project.org/formatparse/datetime). The formats are based on [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details).
</div> </div>
## Min and Max Dates ## Min and Max Dates
Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or Sets the minimum and maximum allowed date values using the `min` and `max` configuration settings (in ISO format or
strtotime()). `strtotime()`).
:::php :::php
DateField::create('MyDate') DateField::create('MyDate')
->setMinDate('-7 days') ->setMinDate('-7 days')
->setMaxDate'2012-12-31') ->setMaxDate('2012-12-31')
## Separate Day / Month / Year Fields ## Separate Day / Month / Year Fields
@ -66,33 +66,21 @@ HTML5 placeholders 'day', 'month' and 'year' are enabled by default.
Any custom date format settings will be ignored. Any custom date format settings will be ignored.
</div> </div>
## Calendar Picker ## Date Picker and HTML5 support
The following setting will add a Calendar to a single DateField, using the jQuery UI DatePicker widget. The field can be used as a [HTML5 input date type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date)
(with `type=date`) by calling `setHTML5(true)`.
:::php :::php
DateField::create('MyDate') DateField::create('MyDate')
->setShowCalendar(true); ->setHTML5(true);
The jQuery date picker will support most custom locale formats (if left as default). In browsers [supporting HTML5 date inputs](caniuse.com/#feat=input-datetime),
If setting an explicit date format via setDateFormat() then the below table of supported this will cause a localised date picker to appear for users.
characters should be used. In this mode, the field will be forced to present and save ISO 8601 date formats (`y-MM-dd`),
since the browser takes care of converting to/from a localised presentation.
It is recommended to use numeric format, as `MMM` or `MMMM` month names may not always pass validation. Browsers without support receive an `<input type=text>` based polyfill.
Constant | xxxxx
-------- | -----
d | numeric day of the month (without leading zero)
dd | numeric day of the month (with leading zero)
EEE | dayname, abbreviated
EEEE | dayname
M | numeric month of the year (without leading zero)
MM | numeric month of the year (with leading zero)
MMM | monthname, abbreviated
MMMM | monthname
y | year (4 digits)
yy | year (2 digits)
yyyy | year (4 digits)
## Formatting Hints ## Formatting Hints

View File

@ -71,18 +71,24 @@ and default alignment of paragraphs and tables to browsers.
### Date and time formats ### Date and time formats
Formats can be set globally in the i18n class. These settings are currently only picked up by the CMS, you'll need Formats can be set globally in the i18n class.
to write your own logic for any frontend output. You can use these settings for your own view logic.
:::php :::php
Config::inst()->update('i18n', 'date_format', 'dd.MM.YYYY'); Config::inst()->update('i18n', 'date_format', 'dd.MM.YYYY');
Config::inst()->update('i18n', 'time_format', 'HH:mm'); Config::inst()->update('i18n', 'time_format', 'HH:mm');
Most localization routines in SilverStripe use the [Zend_Date API](http://framework.zend.com/manual/1.12/en/zend.date.overview.html). Localization in SilverStripe uses PHP's [intl extension](http://php.net/intl).
This means all formats are defined in Formats for it's [IntlDateFormatter](http://php.net/manual/en/class.intldateformatter.php)
[ISO date format](http://framework.zend.com/manual/1.12/en/zend.date.constants.html), are defined in [ICU format](http://www.icu-project.org/apiref/icu4c/classSimpleDateFormat.html#details),
not PHP's built-in [date()](http://nz.php.net/manual/en/function.date.php). not PHP's built-in [date()](http://nz.php.net/manual/en/function.date.php).
These settings are not used for CMS presentation.
Users can choose their own locale, which determines the date format
that gets presented to them. Currently this is a mix of PHP defaults (for readonly `DateField` and `TimeField`),
browser defaults (for `DateField` on browsers supporting HTML5), and [Moment.JS](http://momentjs.com/)
client-side logic (for `DateField` polyfills and other readonly dates and times).
### Language Names ### Language Names
SilverStripe comes with a built-in list of common languages, listed by locale and region. SilverStripe comes with a built-in list of common languages, listed by locale and region.

View File

@ -398,6 +398,18 @@ In templates this can also be invoked as below:
<%t MyObject.PLURALS 'An item|{count} items' count=$Count %> <%t MyObject.PLURALS 'An item|{count} items' count=$Count %>
#### Removed Member.DateFormat and Member.TimeFormat database settings
We're using [native HTML5 date and time pickers](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date)
in `DateField` and `TimeField` now ([discussion](https://github.com/silverstripe/silverstripe-framework/issues/6626)),
where the browser localises the output based on the browser/system preferences.
In this context it no longer makes sense to give users control over their own
date and time formats in their CMS profile.
`Member->getDateFormat()` and `Member->getTimeFormat()` still exist, and default to
the [IntlDateFormatter defaults](http://php.net/manual/en/class.intldateformatter.php) for the selected locale.
#### New asset storage mechanism #### New asset storage mechanism
File system has been abstracted into an abstract interface. By default, the out of the box filesystem File system has been abstracted into an abstract interface. By default, the out of the box filesystem
@ -1495,19 +1507,27 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`:
New `DateField` methods replace `getConfig()` / `setConfig()`: New `DateField` methods replace `getConfig()` / `setConfig()`:
* `getShowCalendar()` / `setShowCalendar()` * `getDateFormat()` / `setDateFormat()`
* `getDateFormat()` / `setShowCalendar()`
* `getMinDate()` / `setMinDate()` * `getMinDate()` / `setMinDate()`
* `getMaxDate()` / `setMaxDate()` * `getMaxDate()` / `setMaxDate()`
* `getPlaceholders()` / `setPlaceholders()`
* `getClientLocale` / `setClientLocale`
* `getLocale()` / `setLocale()` * `getLocale()` / `setLocale()`
* option `dmyfields` is now superceded with an `SeparatedDateField` class
The `DateField` has changed behavior:
* `DateField` no longer provides a jQuery UI date picker,
and uses [HTML5 date pickers](https://www.wufoo.com/html5/types/4-date.html) instead.
Use `setUseHTML()` to activate this mode (instead of `setConfig('showcalendar', true)`).
* `DateField` provides an optional polyfill for
[browsers without HTML5 date picker support](http://caniuse.com/#feat=input-datetime)
* The `dmyfields` option is now superceded with an `SeparatedDateField` class.
* `getPlaceholders()` / `setPlaceholders()` moved to a new `SeparatedDateField` class
* `getClientLocale` / `setClientLocale` have been removed (handled by `DateField->locale` and browser settings)
New `TimeField` methods replace `getConfig()` / `setConfig()` New `TimeField` methods replace `getConfig()` / `setConfig()`
* `getTimeFormat()` / `setTimeFormat()` * `getTimeFormat()` / `setTimeFormat()`
* `getLocale()` / `setLocale()` * `getLocale()` / `setLocale()`
* `getClientConfig()` has been removed (in favour of `setHTML5()`)
#### <a name="overview-template-removed"></a>Template and Form Removed API #### <a name="overview-template-removed"></a>Template and Form Removed API

View File

@ -115,25 +115,27 @@ class DateField extends TextField
protected $rawValue = null; protected $rawValue = null;
/** /**
* Check if calendar should be shown on the frontend * Use HTML5-based input fields (and force ISO 8601 date formats).
* *
* @var bool
*/
protected $html5 = false;
/**
* @return bool * @return bool
*/ */
public function getShowCalendar() public function getHTML5()
{ {
return $this->showCalendar; return $this->html5;
} }
/** /**
* Set if calendar should be shown on the frontend. * @param boolean $bool
* @internal WARNING: Experimental and volatile API.
*
* @param bool $show
* @return $this * @return $this
*/ */
public function setShowCalendar($show) public function setHTML5($bool)
{ {
$this->showCalendar = $show; $this->html5 = $bool;
return $this; return $this;
} }
@ -209,6 +211,7 @@ class DateField extends TextField
/** /**
* Get date formatter with the standard locale / date format * Get date formatter with the standard locale / date format
* *
* @throws \LogicException
* @return IntlDateFormatter * @return IntlDateFormatter
*/ */
protected function getFormatter() protected function getFormatter()
@ -219,8 +222,20 @@ class DateField extends TextField
IntlDateFormatter::NONE IntlDateFormatter::NONE
); );
// Don't invoke getDateFormat() directly to avoid infinite loop $isoFormat = 'y-MM-dd';
if ($this->dateFormat) {
if ($this->dateFormat && $this->getHTML5() && $this->dateFormat !== $isoFormat) {
throw new \LogicException(sprintf(
'Can\'t use a custom dateFormat value with $html5=true (needs to be %s)',
$isoFormat
));
}
if ($this->getHTML5()) {
// Browsers expect ISO 8601 dates, localisation is handled on the client
$formatter->setPattern($isoFormat);
} elseif ($this->dateFormat) {
// Don't invoke getDateFormat() directly to avoid infinite loop
$ok = $formatter->setPattern($this->dateFormat); $ok = $formatter->setPattern($this->dateFormat);
if (!$ok) { if (!$ok) {
throw new InvalidArgumentException("Invalid date format {$this->dateFormat}"); throw new InvalidArgumentException("Invalid date format {$this->dateFormat}");
@ -236,59 +251,38 @@ class DateField extends TextField
*/ */
protected function getISO8601Formatter() protected function getISO8601Formatter()
{ {
$locale = i18n::config()->uninherited('default_locale');
$formatter = IntlDateFormatter::create( $formatter = IntlDateFormatter::create(
i18n::config()->uninherited('default_locale'), i18n::config()->uninherited('default_locale'),
IntlDateFormatter::MEDIUM, IntlDateFormatter::MEDIUM,
IntlDateFormatter::NONE IntlDateFormatter::NONE
); );
$formatter->setLenient(false); $formatter->setLenient(false);
// CLDR iso8601 date. // CLDR ISO 8601 date.
$formatter->setPattern('y-MM-dd'); $formatter->setPattern('y-MM-dd');
return $formatter; return $formatter;
} }
public function FieldHolder($properties = array()) public function FieldHolder($properties = array())
{ {
return $this->renderWithClientView(function () use ($properties) { if ($this->getHTML5()) {
return parent::FieldHolder($properties); // Browsers expect ISO 8601 dates, localisation is handled on the client
}); $this->setDateFormat('y-MM-dd');
}
public function SmallFieldHolder($properties = array())
{
return $this->renderWithClientView(function () use ($properties) {
return parent::SmallFieldHolder($properties);
});
}
/**
* Generate field with client view enabled
*
* @param callable $callback
* @return string
*/
protected function renderWithClientView($callback)
{
$clientView = null;
if ($this->getShowCalendar()) {
$clientView = $this->getClientView();
$clientView->onBeforeRender();
} }
$html = $callback();
if ($clientView) { return parent::FieldHolder($properties);
$html = $clientView->onAfterRender($html);
}
return $html;
} }
public function getAttributes() public function getAttributes()
{ {
$attributes = parent::getAttributes(); $attributes = parent::getAttributes();
// Merge with client config $attributes['lang'] = i18n::convert_rfc1766($this->getLocale());
$config = $this->getClientConfig();
foreach ($config as $key => $value) { if ($this->getHTML5()) {
$attributes["data-{$key}"] = $value; $attributes['type'] = 'date';
$attributes['min'] = $this->getMinDate();
$attributes['max'] = $this->getMaxDate();
} }
return $attributes; return $attributes;
@ -438,29 +432,6 @@ class DateField extends TextField
return $this; return $this;
} }
/**
* Get locale code for client-side. Will default to getLocale() if omitted.
*
* @return string
*/
public function getClientLocale()
{
if ($this->clientLocale) {
return $this->clientLocale;
}
return $this->getLocale();
}
/**
* @param string $clientLocale
* @return DateField
*/
public function setClientLocale($clientLocale)
{
$this->clientLocale = $clientLocale;
return $this;
}
public function getSchemaValidation() public function getSchemaValidation()
{ {
$rules = parent::getSchemaValidation(); $rules = parent::getSchemaValidation();
@ -504,35 +475,6 @@ class DateField extends TextField
return $this; return $this;
} }
/**
* Get client data properties for this field
*
* @return array
*/
public function getClientConfig()
{
$view = $this->getClientView();
$config = [
'showcalendar' => $this->getShowCalendar() ? 'true' : null,
'date-format' => $view->getDateFormat(), // https://api.jqueryui.com/datepicker/#option-dateFormat
'locale' => $view->getLocale(),
];
// Format min/maxDate in format expected by jquery datepicker
$min = $this->getMinDate();
if ($min) {
// https://api.jqueryui.com/datepicker/#option-minDate
$config['min-date'] = $this->iso8601ToLocalised($min);
}
$max = $this->getMaxDate();
if ($max) {
// https://api.jqueryui.com/datepicker/#option-maxDate
$config['max-date'] = $this->iso8601ToLocalised($max);
}
return $config;
}
/** /**
* Convert date localised in the current locale to ISO 8601 date * Convert date localised in the current locale to ISO 8601 date
* *
@ -599,11 +541,4 @@ class DateField extends TextField
return $formatter->format($timestamp); return $formatter->format($timestamp);
} }
/**
* @return DateField_View_JQuery
*/
protected function getClientView()
{
return DateField_View_JQuery::create($this);
}
} }

View File

@ -1,197 +0,0 @@
<?php
namespace SilverStripe\Forms;
use InvalidArgumentException;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\i18n\i18n;
use SilverStripe\View\Requirements;
/**
* 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.
*/
class DateField_View_JQuery
{
use Injectable;
use Configurable;
/**
* @var DateField
*/
protected $field;
/**
* @var array Maps values from {@link i18n::$all_locales} to
* localizations existing in jQuery UI.
*/
private static $locale_map = array(
'en_GB' => 'en-GB',
'en_US' => 'en',
'en_NZ' => 'en-GB',
'fr_CH' => 'fr',
'pt_BR' => 'pt-BR',
'sr_SR' => 'sr-SR',
'zh_CN' => 'zh-CN',
'zh_HK' => 'zh-HK',
'zh_TW' => 'zh-TW',
);
/**
* @param DateField $field
*/
public function __construct($field)
{
$this->field = $field;
// Health check
if (!$this->localePath('en')) {
throw new InvalidArgumentException("Missing jquery config");
}
}
/**
* @return DateField
*/
public function getField()
{
return $this->field;
}
/**
* Get path to localisation file for a given locale, if it exists
*
* @param string $lang
* @return string Relative path to file, or null if it isn't available
*/
protected function localePath($lang)
{
$path = ADMIN_THIRDPARTY_DIR . "/jquery-ui/datepicker/i18n/jquery.ui.datepicker-{$lang}.js";
if (file_exists(BASE_PATH . '/' . $path)) {
return $path;
}
return null;
}
public function onBeforeRender()
{
}
/**
* @param String $html
* @return string
*/
public function onAfterRender($html)
{
if ($this->getField()->getShowCalendar()) {
// Load config for this locale if available
$locale = $this->getLocale();
$localeFile = $this->localePath($locale);
if ($localeFile) {
Requirements::javascript($localeFile);
}
}
return $html;
}
/**
* Determines which language to use for jQuery UI, which
* can be different from the value set in i18n.
*
* @return string
*/
public function getLocale()
{
$locale = $this->getField()->getClientLocale();
// Check standard mappings
$map = Config::inst()->get(__CLASS__, 'locale_map');
if (array_key_exists($locale, $map)) {
return $map[$locale];
}
// Fall back to default lang (meaning "en_US" turns into "en")
return i18n::getData()->langFromLocale($locale);
}
/**
* 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
* From http://userguide.icu-project.org/formatparse/datetime
*
* @param string $format
* @return string
*/
public static function convert_iso_to_jquery_format($format)
{
$convert = array(
'/([^d])d([^d])/' => '$1d$2',
'/^d([^d])/' => 'd$1',
'/([^d])d$/' => '$1d',
'/dd/' => 'dd',
'/SS/' => '',
'/eee/' => 'd',
'/e/' => 'N',
'/D/' => '',
'/EEEE/' => 'DD',
'/EEE/' => 'D',
'/w/' => '',
// make single "M" lowercase
'/([^M])M([^M])/' => '$1m$2',
// make single "M" at start of line lowercase
'/^M([^M])/' => 'm$1',
// make single "M" at end of line lowercase
'/([^M])M$/' => '$1m',
// match exactly three capital Ms not preceeded or followed by an M
'/(?<!M)MMM(?!M)/' => 'M',
// match exactly two capital Ms not preceeded or followed by an M
'/(?<!M)MM(?!M)/' => 'mm',
// match four capital Ms (maximum allowed)
'/MMMM/' => 'MM',
'/l/' => '',
'/YYYY/' => 'yy',
'/yyyy/' => 'yy',
// See http://open.silverstripe.org/ticket/7669
'/y{1,3}/' => 'yy',
'/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);
}
/**
* Get client date format
*
* @return string
*/
public function getDateFormat()
{
return static::convert_iso_to_jquery_format($this->getField()->getDateFormat());
}
}

View File

@ -13,6 +13,10 @@ use SilverStripe\i18n\i18n;
* If you want to save into {@link Date} or {@link Time} columns, * If you want to save into {@link Date} or {@link Time} columns,
* please instanciate the fields separately. * please instanciate the fields separately.
* *
* This field does not implement the <input type="datetime-local"> HTML5 field,
* but can use date and time HTML5 inputs separately (through {@link DateField->setHTML5()}
* and {@link TimeField->setHTML5()}.
*
* # Configuration * # Configuration
* *
* Individual options are configured either on the DatetimeField, or on individual * Individual options are configured either on the DatetimeField, or on individual

View File

@ -57,6 +57,31 @@ class TimeField extends TextField
*/ */
protected $timezone = null; protected $timezone = null;
/**
* Use HTML5-based input fields (and force ISO 8601 date formats).
*
* @var bool
*/
protected $html5 = false;
/**
* @return bool
*/
public function getHTML5()
{
return $this->html5;
}
/**
* @param boolean $bool
* @return $this
*/
public function setHTML5($bool)
{
$this->html5 = $bool;
return $this;
}
/** /**
* Get time format in CLDR standard format * Get time format in CLDR standard format
* *
@ -140,8 +165,20 @@ class TimeField extends TextField
$this->getTimezone() $this->getTimezone()
); );
// Don't invoke getDateFormat() directly to avoid infinite loop $isoFormat = 'HH:mm:ss';
if ($this->timeFormat) {
if ($this->timeFormat && $this->getHTML5() && $this->timeFormat !== $isoFormat) {
throw new \LogicException(sprintf(
'Can\'t use a custom timeFormat value with $html5=true (needs to be %s)',
$isoFormat
));
}
if ($this->getHTML5()) {
// Browsers expect ISO 8601 times, localisation is handled on the client
$formatter->setPattern($isoFormat);
// Don't invoke getTimeFormat() directly to avoid infinite loop
} elseif ($this->timeFormat) {
$ok = $formatter->setPattern($this->timeFormat); $ok = $formatter->setPattern($this->timeFormat);
if (!$ok) { if (!$ok) {
throw new InvalidArgumentException("Invalid time format {$this->timeFormat}"); throw new InvalidArgumentException("Invalid time format {$this->timeFormat}");
@ -164,35 +201,21 @@ class TimeField extends TextField
date_default_timezone_get() // Default to server timezone date_default_timezone_get() // Default to server timezone
); );
$formatter->setLenient(false); $formatter->setLenient(false);
// CLDR iso8601 time // ISO 8601 time
// Note we omit timezone from this format, and we assume server TZ always. // Note we omit timezone from this format, and we assume server TZ always.
$formatter->setPattern('HH:mm:ss'); $formatter->setPattern('HH:mm:ss');
return $formatter; return $formatter;
} }
public function getAttribute($name) public function getAttributes()
{ {
$attributes = parent::getAttributes(); $attributes = parent::getAttributes();
// Merge with client config if ($this->getHTML5()) {
$config = $this->getClientConfig(); $attributes['type'] = 'time';
foreach ($config as $key => $value) {
$attributes["data-{$key}"] = $value;
} }
return $attributes;
}
/** return $attributes;
* Get client config options for this field
*
* @return array
*/
public function getClientConfig()
{
return [
// @todo - Support javascript time picker
'timeformat' => $this->getTimeFormat(),
];
} }
public function Type() public function Type()
@ -337,9 +360,19 @@ class TimeField extends TextField
$fromFormatter = $this->getFormatter(); $fromFormatter = $this->getFormatter();
$toFormatter = $this->getISO8601Formatter(); $toFormatter = $this->getISO8601Formatter();
$timestamp = $fromFormatter->parse($time); $timestamp = $fromFormatter->parse($time);
// Try to parse time without seconds, since that's a valid HTML5 submission format
// See https://html.spec.whatwg.org/multipage/infrastructure.html#times
if ($timestamp === false && $this->setHTML5(true)) {
$fromFormatter->setPattern('HH:mm');
$timestamp = $fromFormatter->parse($time);
}
// If timestamp still can't be detected, we've got an invalid time
if ($timestamp === false) { if ($timestamp === false) {
return null; return null;
} }
return $toFormatter->format($timestamp); return $toFormatter->format($timestamp);
} }

View File

@ -512,17 +512,7 @@ class DBDate extends DBField
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
$field = DateField::create($this->name, $title); $field = DateField::create($this->name, $title);
$format = $field->getDateFormat(); $field->setHTML5(true);
// Show formatting hints for better usability
$now = DBDatetime::now()->Format($format);
$field->setDescription(_t(
'FormField.EXAMPLE',
'e.g. {format}',
'Example format',
[ 'format' => $now ]
));
$field->setAttribute('placeholder', $format);
return $field; return $field;
} }

View File

@ -142,17 +142,8 @@ class DBTime extends DBField
public function scaffoldFormField($title = null, $params = null) public function scaffoldFormField($title = null, $params = null)
{ {
$field = TimeField::create($this->name, $title); $field = TimeField::create($this->name, $title);
$format = $field->getTimeFormat(); $field->setHTML5(true);
// Show formatting hints for better usability
$now = DBDatetime::now()->Format($format);
$field->setDescription(_t(
'FormField.Example',
'e.g. {format}',
'Example format',
[ 'format' => $now ]
));
$field->setAttribute('placeholder', $format);
return $field; return $field;
} }

View File

@ -19,7 +19,6 @@ use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\FieldList; use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig; use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
use SilverStripe\Forms\ListboxField; use SilverStripe\Forms\ListboxField;
use SilverStripe\Forms\MemberDatetimeOptionsetField;
use SilverStripe\i18n\i18n; use SilverStripe\i18n\i18n;
use SilverStripe\MSSQL\MSSQLDatabase; use SilverStripe\MSSQL\MSSQLDatabase;
use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\ArrayList;
@ -81,9 +80,6 @@ class Member extends DataObject implements TemplateGlobalProvider
'Locale' => 'Varchar(6)', 'Locale' => 'Varchar(6)',
// handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set // handled in registerFailedLogin(), only used if $lock_out_after_incorrect_logins is set
'FailedLoginCount' => 'Int', 'FailedLoginCount' => 'Int',
// In ISO format
'DateFormat' => 'Varchar(30)',
'TimeFormat' => 'Varchar(30)',
); );
private static $belongs_many_many = array( private static $belongs_many_many = array(
@ -1315,19 +1311,16 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
/** /**
* Override the default getter for DateFormat so the * Return the date format based on the user's chosen locale,
* default format for the user's locale is used * falling back to the default format defined by the {@link i18n.get_locale()} setting.
* if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getDateFormat() public function getDateFormat()
{ {
$format = $this->getField('DateFormat'); $format = $this->getDefaultDateFormat();
if ($format) { $this->extend('updateDateFormat', $format);
return $format; return $format;
}
return $this->getDefaultDateFormat();
} }
/** /**
@ -1343,19 +1336,17 @@ class Member extends DataObject implements TemplateGlobalProvider
} }
/** /**
* Override the default getter for TimeFormat so the * Return the time format based on the user's chosen locale,
* default format for the user's locale is used * falling back to the default format defined by the {@link i18n.get_locale()} setting.
* if the user has not defined their own.
* *
* @return string ISO date format * @return string ISO date format
*/ */
public function getTimeFormat() public function getTimeFormat()
{ {
$timeFormat = $this->getField('TimeFormat'); $format = $this->getDefaultTimeFormat();
if ($timeFormat) { $this->extend('updateTimeFormat', $format);
return $timeFormat;
} return $format;
return $this->getDefaultTimeFormat();
} }
//---------------------------------------------------------------------// //---------------------------------------------------------------------//
@ -1592,61 +1583,11 @@ class Member extends DataObject implements TemplateGlobalProvider
if ($permissionsTab) { if ($permissionsTab) {
$permissionsTab->addExtraClass('readonly'); $permissionsTab->addExtraClass('readonly');
} }
// Date format selecter
$mainFields->push(
$dateFormatField = new MemberDatetimeOptionsetField(
'DateFormat',
$this->fieldLabel('DateFormat'),
$this->getDateFormats()
)
);
$formatClass = get_class($dateFormatField);
$dateFormatField->setValue($this->DateFormat);
$dateTemplate = SSViewer::get_templates_by_class($formatClass, '_description_date', $formatClass);
$dateFormatField->setDescriptionTemplate($dateTemplate);
// Time format selector
$mainFields->push(
$timeFormatField = new MemberDatetimeOptionsetField(
'TimeFormat',
$this->fieldLabel('TimeFormat'),
$this->getTimeFormats()
)
);
$timeFormatField->setValue($this->TimeFormat);
$timeTemplate = SSViewer::get_templates_by_class($formatClass, '_description_time', $formatClass);
$timeFormatField->setDescriptionTemplate($timeTemplate);
}); });
return parent::getCMSFields(); return parent::getCMSFields();
} }
/**
* Get list of date formats with example values
*
* @return array
*/
protected function getDateFormats()
{
$defaultDateFormat = $this->getDefaultDateFormat();
$formats = [
'MMM d, y' => null,
'yyyy/MM/dd' => null,
'MM/dd/y' => null,
'dd/MM/y' => null,
];
unset($formats[$defaultDateFormat]);
$formats[$defaultDateFormat] = null;
// Fill in each format with example
foreach (array_keys($formats) as $format) {
$formats[$format] = DBDatetime::now()->Format($format);
}
// Mark default format
$formats[$defaultDateFormat] .= sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
return $formats;
}
/** /**
* @return string * @return string
*/ */
@ -1674,30 +1615,6 @@ class Member extends DataObject implements TemplateGlobalProvider
return $defaultTimeFormat; return $defaultTimeFormat;
} }
/**
* Get list of date formats with example values
*
* @return array
*/
protected function getTimeFormats()
{
$defaultTimeFormat = $this->getDefaultTimeFormat();
$formats = [
'h:mm a' => null,
'H:mm' => null,
];
unset($formats[$defaultTimeFormat]);
$formats[$defaultTimeFormat] = null;
// Fill in each format with example
foreach (array_keys($formats) as $format) {
$formats[$format] = DBDatetime::now()->Format($format);
}
// Mark default format
$formats[$defaultTimeFormat] .= sprintf(' (%s)', _t('Member.DefaultDateTime', 'default'));
return $formats;
}
/** /**
* @param bool $includerelations Indicate if the labels returned include relation fields * @param bool $includerelations Indicate if the labels returned include relation fields
* @return array * @return array
@ -1714,8 +1631,6 @@ class Member extends DataObject implements TemplateGlobalProvider
$labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date'); $labels['PasswordExpiry'] = _t('Member.db_PasswordExpiry', 'Password Expiry Date', 'Password expiry date');
$labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date'); $labels['LockedOutUntil'] = _t('Member.db_LockedOutUntil', 'Locked out until', 'Security related date');
$labels['Locale'] = _t('Member.db_Locale', 'Interface Locale'); $labels['Locale'] = _t('Member.db_Locale', 'Interface Locale');
$labels['DateFormat'] = _t('Member.DATEFORMAT', 'Date format');
$labels['TimeFormat'] = _t('Member.TIMEFORMAT', 'Time format');
if ($includerelations) { if ($includerelations) {
$labels['Groups'] = _t( $labels['Groups'] = _t(
'Member.belongs_many_many_Groups', 'Member.belongs_many_many_Groups',

View File

@ -85,12 +85,20 @@ class i18n implements TemplateGlobalProvider
private static $default_locale = 'en_US'; private static $default_locale = 'en_US';
/** /**
* System-wide date format. Will be overruled for CMS UI display
* by the format defaults inferred from the browser as well as
* any user-specific locale preferences.
*
* @config * @config
* @var string * @var string
*/ */
private static $date_format = 'yyyy-MM-dd'; private static $date_format = 'yyyy-MM-dd';
/** /**
* System-wide time format. Will be overruled for CMS UI display
* by the format defaults inferred from the browser as well as
* any user-specific locale preferences.
*
* @config * @config
* @var string * @var string
*/ */

View File

@ -259,4 +259,16 @@ class DateFieldTest extends SapphireTest
"Even if input value hasn't got leading 0's in it we still get the correct data value" "Even if input value hasn't got leading 0's in it we still get the correct data value"
); );
} }
/**
* @expectedException \LogicException
*/
public function testHtml5WithCustomFormatThrowsException()
{
$dateField = new DateField('Date', 'Date');
$dateField->setValue('2010-03-31');
$dateField->setHTML5(true);
$dateField->setDateFormat('d/M/y');
$dateField->Value();
}
} }

View File

@ -1,47 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\DateField_View_JQuery;
class DateFieldViewJQueryTest extends SapphireTest
{
public 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.m.yy',
DateField_View_JQuery::convert_iso_to_jquery_format('dd.M.yyyy'),
'Month, no leading zero'
);
$this->assertEquals(
'dd.mm.yy',
DateField_View_JQuery::convert_iso_to_jquery_format('dd.MM.yyyy'),
'Month, two digit'
);
$this->assertEquals(
'dd.M.yy',
DateField_View_JQuery::convert_iso_to_jquery_format('dd.MMM.yyyy'),
'Abbreviated month name'
);
$this->assertEquals(
'dd.MM.yy',
DateField_View_JQuery::convert_iso_to_jquery_format('dd.MMMM.yyyy'),
'Full month name'
);
}
}

View File

@ -1,177 +0,0 @@
<?php
namespace SilverStripe\Forms\Tests;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\CSSContentParser;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Control\Controller;
use SilverStripe\Forms\MemberDatetimeOptionsetField;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\RequiredFields;
use SilverStripe\i18n\i18n;
use SilverStripe\ORM\FieldType\DBDatetime;
use SilverStripe\Security\Member;
class MemberDatetimeOptionsetFieldTest extends SapphireTest
{
protected static $fixture_file = 'MemberDatetimeOptionsetFieldTest.yml';
/**
* @param Member $member
* @return MemberDatetimeOptionsetField
*/
protected function createDateFormatFieldForMember($member)
{
$defaultDateFormat = $member->getDefaultDateFormat();
$dateFormatMap = array(
'yyyy-MM-dd' => DBDatetime::now()->Format('yyyy-MM-dd'),
'yyyy/MM/dd' => DBDatetime::now()->Format('yyyy/MM/dd'),
'MM/dd/yyyy' => DBDatetime::now()->Format('MM/dd/yyyy'),
'dd/MM/yyyy' => DBDatetime::now()->Format('dd/MM/yyyy'),
);
$dateFormatMap[$defaultDateFormat] = DBDatetime::now()->Format($defaultDateFormat) . ' (default)';
$field = new MemberDatetimeOptionsetField(
'DateFormat',
'Date format',
$dateFormatMap
);
$field->setValue($member->getDateFormat());
return $field;
}
/**
* @param Member $member
* @return MemberDatetimeOptionsetField
*/
protected function createTimeFormatFieldForMember($member)
{
$defaultTimeFormat = $member->getDefaultTimeFormat();
$timeFormatMap = array(
'h:mm a' => DBDatetime::now()->Format('h:mm a'),
'H:mm' => DBDatetime::now()->Format('H:mm'),
);
$timeFormatMap[$defaultTimeFormat] = DBDatetime::now()->Format($defaultTimeFormat) . ' (default)';
$field = new MemberDatetimeOptionsetField(
'TimeFormat',
'Time format',
$timeFormatMap
);
$field->setValue($member->getTimeFormat());
return $field;
}
public function testDateFormatDefaultCheckedInFormField()
{
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'noformatmember');
$field = $this->createDateFormatFieldForMember($member);
/** @skipUpgrade */
$field->setForm(
new Form(
new Controller(),
'Form',
new FieldList(),
new FieldList()
)
); // fake form
// `MMM d, y` is default format for default locale (en_US)
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MMM_d_y');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
public function testTimeFormatDefaultCheckedInFormField()
{
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'noformatmember');
$field = $this->createTimeFormatFieldForMember($member);
/** @skipUpgrade */
$field->setForm(
new Form(
new Controller(),
'Form',
new FieldList(),
new FieldList()
)
); // fake form
// `h:mm:ss a` is the default for en_US locale
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_h:mm:ss_a');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
public function testDateFormatChosenIsCheckedInFormField()
{
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'noformatmember');
$member->setField('DateFormat', 'MM/dd/yyyy');
$field = $this->createDateFormatFieldForMember($member);
/** @skipUpgrade */
$field->setForm(
new Form(
new Controller(),
'Form',
new FieldList(),
new FieldList()
)
); // fake form
$parser = new CSSContentParser($field->Field());
$xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MM_dd_yyyy');
$this->assertEquals('checked', (string) $xmlArr[0]['checked']);
}
public function testDateFormatCustomFormatAppearsInCustomInputInField()
{
/** @var Member $member */
$member = $this->objFromFixture(Member::class, 'noformatmember');
$member->setField('DateFormat', 'dd MM yy');
$field = $this->createDateFormatFieldForMember($member);
/** @skipUpgrade */
$field->setForm(
new Form(
new Controller(),
'Form',
new FieldList(),
new FieldList()
)
); // fake form
$parser = new CSSContentParser($field->Field());
$xmlInputArr = $parser->getBySelector('.valcustom input');
$this->assertEquals('checked', (string) $xmlInputArr[0]['checked']);
$this->assertEquals('dd MM yy', (string) $xmlInputArr[1]['value']);
}
public function testDateFormValid()
{
$field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat');
$validator = new RequiredFields();
$this->assertTrue($field->validate($validator));
$field->setSubmittedValue([
'Options' => '__custom__',
'Custom' => 'dd MM yyyy'
]);
$this->assertTrue($field->validate($validator));
$field->setSubmittedValue([
'Options' => '__custom__',
'Custom' => 'sdfdsfdfd1244'
]);
// @todo - Be less forgiving of invalid CLDR date format strings
$this->assertTrue($field->validate($validator));
}
public function testDescriptionTemplate()
{
$field = new MemberDatetimeOptionsetField('DateFormat', 'DateFormat');
$this->assertEmpty($field->getDescription());
$field->setDescription('Test description');
$this->assertEquals('Test description', $field->getDescription());
$field->setDescriptionTemplate(get_class($field).'_description_time');
$this->assertNotEmpty($field->getDescription());
$this->assertNotEquals('Test description', $field->getDescription());
}
}

View File

@ -1,6 +0,0 @@
SilverStripe\Security\Member:
noformatmember:
Email: noformat@test.com
delocalemember:
Email: delocalemember@test.com
Locale: de_DE

View File

@ -40,6 +40,17 @@ class TimeFieldTest extends SapphireTest
$this->assertFalse($f->validate(new RequiredFields())); $this->assertFalse($f->validate(new RequiredFields()));
} }
public function testValidateLenientWithHtml5()
{
$f = new TimeField('Time', 'Time', '23:59:59');
$f->setHTML5(true);
$this->assertTrue($f->validate(new RequiredFields()));
$f = new TimeField('Time', 'Time', '23:59'); // leave out seconds
$f->setHTML5(true);
$this->assertTrue($f->validate(new RequiredFields()));
}
public function testSetLocale() public function testSetLocale()
{ {
// should get en_NZ by default through setUp() // should get en_NZ by default through setUp()
@ -123,4 +134,24 @@ class TimeFieldTest extends SapphireTest
$f->setValue('03:59:00'); $f->setValue('03:59:00');
$this->assertEquals($f->dataValue(), '03:59:00'); $this->assertEquals($f->dataValue(), '03:59:00');
} }
public function testLenientSubmissionParseWithoutSecondsOnHtml5()
{
$f = new TimeField('Time', 'Time');
$f->setHTML5(true);
$f->setSubmittedValue('23:59');
$this->assertEquals($f->Value(), '23:59:00');
}
/**
* @expectedException \LogicException
*/
public function testHtml5WithCustomFormatThrowsException()
{
$f = new TimeField('Time', 'Time');
$f->setValue('15:59:00');
$f->setHTML5(true);
$f->setTimeFormat('mm:HH');
$f->Value();
}
} }