From 014f0d23edbb604ee285274711b2c49124acc3f7 Mon Sep 17 00:00:00 2001 From: Damian Mooyman Date: Tue, 14 Feb 2017 18:19:09 +1300 Subject: [PATCH] API Create SeparatedDateField API Restrict allowed values parsed via DBDate::setValue API Remove NumericField_Readonly API Remove DBTime::Nice12 / Nice24 --- admin/client/dist/styles/bundle.css | 7 +- admin/client/src/styles/legacy/_forms.scss | 6 +- .../03_Forms/01_Validation.md | 7 +- .../03_Forms/Field_types/02_DateField.md | 48 +++--- docs/en/04_Changelogs/4.0.0.md | 63 ++++--- src/Forms/DateField.php | 159 +----------------- src/Forms/HTMLEditor/TinyMCEConfig.php | 1 - src/Forms/MemberDatetimeOptionsetField.php | 2 +- src/Forms/MoneyField.php | 6 +- src/Forms/NumericField.php | 7 + src/Forms/NumericField_Readonly.php | 30 ---- src/Forms/SeparatedDateField.php | 136 +++++++++++++++ src/ORM/FieldType/DBDate.php | 105 ++++-------- src/ORM/FieldType/DBTime.php | 20 --- src/i18n/Data/Intl/IntlLocales.php | 4 +- tests/php/Forms/DateFieldTest.php | 17 +- tests/php/Forms/DatetimeFieldTest.php | 3 +- tests/php/Forms/NumericFieldTest.php | 8 +- tests/php/ORM/DBDateTest.php | 79 +-------- tests/php/ORM/DBTimeTest.php | 12 -- 20 files changed, 274 insertions(+), 446 deletions(-) delete mode 100644 src/Forms/NumericField_Readonly.php create mode 100644 src/Forms/SeparatedDateField.php diff --git a/admin/client/dist/styles/bundle.css b/admin/client/dist/styles/bundle.css index b836322e7d4..d323dbf7e2c 100644 --- a/admin/client/dist/styles/bundle.css +++ b/admin/client/dist/styles/bundle.css @@ -8859,11 +8859,16 @@ fieldset{ height:18px; } -.field input.day,.field input.month,.field input.year{ +.field input.day,.field input.month{ width:56px; display:inline; } +.field input.year{ + width:72px; + display:inline; +} + .field input.time{ width:88px; } diff --git a/admin/client/src/styles/legacy/_forms.scss b/admin/client/src/styles/legacy/_forms.scss index 36fb6508d23..dbcb5038ce3 100644 --- a/admin/client/src/styles/legacy/_forms.scss +++ b/admin/client/src/styles/legacy/_forms.scss @@ -295,10 +295,14 @@ form.small .field, .field.small { } /* Date Fields */ - input.month, input.day, input.year { + input.month, input.day { width: ($grid-x * 7); display: inline; } + input.year { + width: ($grid-x * 9); + display: inline; + } input.time { width: ($grid-x * 11); // smaller time field, since input is restricted diff --git a/docs/en/02_Developer_Guides/03_Forms/01_Validation.md b/docs/en/02_Developer_Guides/03_Forms/01_Validation.md index 61422cd3d69..a757f42be65 100644 --- a/docs/en/02_Developer_Guides/03_Forms/01_Validation.md +++ b/docs/en/02_Developer_Guides/03_Forms/01_Validation.md @@ -66,12 +66,7 @@ Each individual [api:FormField] instance is responsible for validating the submi Subclasses of `FormField` can define their own version of `validate` to provide custom validation rules such as the above example with the `Email` validation. The `validate` method on `FormField` takes a single argument of the current -`Validator` instance. - -
-The data value of the `FormField` submitted is not passed into validate. It is stored in the `value` property through -the `setValue` method. -
+`Validator` instance. ```php public function validate($validator) diff --git a/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md b/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md index d02519acadf..71946f46f2f 100644 --- a/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md +++ b/docs/en/02_Developer_Guides/03_Forms/Field_types/02_DateField.md @@ -3,8 +3,8 @@ summary: How to format and use the DateField class. # DateField -This `FormField` subclass lets you display an editable date, either in a single text input field, or in three separate -fields for day, month and year. It also provides a calendar date picker. +This `FormField` subclass lets you display an editable date, in a single text input field. +It also provides a calendar date picker. The following example will add a simple DateField to your Page, allowing you to enter a date manually. @@ -33,14 +33,14 @@ The following example will add a simple DateField to your Page, allowing you to ## Custom Date Format -A custom date format for a [api:DateField] can be provided through `setConfig`. +A custom date format for a [api:DateField] can be provided through `setDateFormat`. :::php // will display a date in the following format: 31-06-2012 DateField::create('MyDate')->setDateFormat('dd-MM-yyyy');
-The formats are based on [Zend_Date constants](http://framework.zend.com/manual/1.12/en/zend.date.constants.html). +The formats are based on [CLDR format](http://userguide.icu-project.org/formatparse/datetime).
@@ -51,19 +51,16 @@ strtotime()). :::php DateField::create('MyDate') - ->setConfig('min', '-7 days') - ->setConfig('max', '2012-12-31') + ->setMinDate('-7 days') + ->setMaxDate'2012-12-31') ## Separate Day / Month / Year Fields -The following setting will display your DateField as three input fields for day, month and year separately. HTML5 -placeholders 'day', 'month' and 'year' are enabled by default. +To display separate input fields for day, month and year separately you can use the `DateFieldSeparated` subclass`. +HTML5 placeholders 'day', 'month' and 'year' are enabled by default. :::php - DateField::create('MyDate') - ->setConfig('dmyfields', true) - ->setConfig('dmyseparator', '/') // set the separator - ->setConfig('dmyplaceholders', 'true'); // enable HTML 5 Placeholders + DateFieldSeparated::create('MyDate');
Any custom date format settings will be ignored. @@ -75,10 +72,13 @@ The following setting will add a Calendar to a single DateField, using the jQuer :::php DateField::create('MyDate') - ->setConfig('showcalendar', true); + ->setShowCalendar(true); + +The jQuery date picker will support most custom locale formats (if left as default). +If setting an explicit date format via setDateFormat() then the below table of supported +characters should be used. -The jQuery DatePicker doesn't support every constant available for `Zend_Date`. If you choose to use the calendar, the -following constants should at least be safe: +It is recommended to use numeric format, as `MMM` or `MMMM` month names may not always pass validation. Constant | xxxxx -------- | ----- @@ -94,15 +94,6 @@ y | year (4 digits) yy | year (2 digits) yyyy | year (4 digits) -Unfortunately the day- and monthname values in Zend Date do not always match those in the existing jQuery UI locale -files, so constants like `EEE` or `MMM`, for day and month names could break validation. To fix this we had to slightly -alter the jQuery locale files, situated in */framework/thirdparty/jquery-ui/datepicker/i18n/*, to match Zend_Date. - -
-At this moment not all locale files may be present. If a locale file is missing, the DatePicker calendar will fallback -to 'yyyy-MM-dd' whenever day - and/or monthnames are used. After saving, the correct format will be displayed. -
- ## Formatting Hints It's often not immediate apparent which format a field accepts, and showing the technical format (e.g. `HH:mm:ss`) is @@ -113,13 +104,14 @@ field description as an example. $dateField = DateField::create('MyDate'); // Show long format as text below the field - $dateField->setDescription(sprintf( - _t('FormField.Example', 'e.g. %s', 'Example format'), - Convert::raw2xml(Zend_Date::now()->toString($dateField->getConfig('dateformat'))) + $dateField->setDescription(_t( + 'FormField.Example', + 'e.g. {format}', + [ 'format' => $dateField->getDateFormat() ] )); // Alternatively, set short format as a placeholder in the field - $dateField->setAttribute('placeholder', $dateField->getConfig('dateformat')); + $dateField->setAttribute('placeholder', $dateField->getDateFormat());
Fields scaffolded through [api:DataObject::scaffoldCMSFields()] automatically have a description attached to them. diff --git a/docs/en/04_Changelogs/4.0.0.md b/docs/en/04_Changelogs/4.0.0.md index 1938f200c2d..f7a341e6e61 100644 --- a/docs/en/04_Changelogs/4.0.0.md +++ b/docs/en/04_Changelogs/4.0.0.md @@ -942,6 +942,9 @@ specific functions. by the array key, or the `class` parameter value. * Uniqueness checks for `File.Name` is performed on write only (not in `setName()`) * Created `Resettable` interface to better declare objects which should be reset between tests. +* Added a server requirement for the php-intl extension (shipped by default with most PHP distributions) +* Replaced Zend_Date and Zend_Locale with the php-intl extension. +* Consistently use CLDR date formats (rather than a mix of CLDR and date() formats) #### General and Core Removed API @@ -1052,17 +1055,23 @@ A very small number of methods were chosen for deprecation, and will be removed * `ChangeSet` and `ChangeSetItem` have been added for batch publishing of versioned dataobjects. * `DataObject.table_name` config can now be used to customise the database table for any record. * `DataObjectSchema` class added to assist with mapping between classes and tables. -* `DBMoney` values are now treated as empty only Amount is null. Values without Currency - will be formatted in the default locale. +* `DBMoney` values are now treated as empty only if `Amount` field is null. If an `Amount` value + is provided without a `Currency` specified, it will be formatted as per the current locale. The below methods have been added or had their functionality updated to `DBDate`, `DBTime` and `DBDatetime` * `getTimestamp()` added to get the respective date / time as unix timestamp (seconds since 1970-01-01) -* `Format()` method now use CLDR format strings, rather than PHP format strings. - See http://userguide.icu-project.org/formatparse/datetime. +* `Format()` method now use [CLDR format strings](http://userguide.icu-project.org/formatparse/datetime), + rather than [PHP format string](http://php.net/manual/en/function.date.php). + E.g. `d/m/Y H:i:s` (php format) should be replaced with to `dd/MM/y HH:mm:ss` (CLDR format). * getISOFormat() added which returns the standard date/time ISO 8601 pattern in CLDR format. -* Dates passed in m/d/y format will now raise a notice but will be parsed. - Dates passed to constructors should follow ISO 8601 (y-m-d). -* 2-digit years will raise a notice. +* `setValue` method is now a lot more restrictive, and expects dates and times to be passed in + ISO 8601 format (y-MM-dd) or (HH:mm:ss). Certain date formats will attempt to parse with + the below restrictions: + - `/`, `.` or `-` are supported date separators, but will be replaced with `-` internally. + - US date formats (m-d-y / y-d-m) will not be supported and may be parsed incorrectly. + (Note: Date form fields will still support localised date formats). + - `dd-MM-y` will be converted to `y-MM-dd` internally. + - 2-digit values for year will now raise errors. * `FormatFromSettings` will default to `Nice()` format if no member is logged in. * `Nice`, `Long` and `Full` methods will now follow standard formatting rules for the current locale, rather than pre-defined formats. @@ -1071,7 +1080,6 @@ The below methods have been added or had their functionality updated to `DBDate` `DBTime` specific changes: * Added `DBTime::FormatFromSettings` -* Added `DBTime::Nice12` #### ORM Removed API @@ -1115,7 +1123,9 @@ The below methods have been added or had their functionality updated to `DBDate` - `days_between` * `nice_format` has been removed from `DBDate` / `DBTime` / `DBDatetime` has been removed in favour of locale-specific formatting for Nice() -* Removed `DBTime::TwelveHour` +* Removed several `DBTime` methods: + - `TwelveHour` + - `Nice24` * Removed some `DBMoney` methods due to lack of support in php-intl. - `NiceWithShortname` - `NiceWithName` @@ -1235,6 +1245,9 @@ The following filesystem synchronisation methods and tasks are also removed * Introduced `AssetAdmin\Forms\UploadField` as a react-friendly version of UploadField. This may also be used in normal entwine forms for managing files in a similar way to UploadField. However, this does not support inline editing of files. +* Added method `FormField::setSubmittedValue($value, $data)` to process input submitted from form + submission, in contrast to `FormField::setValue($value, $data)` which is intended to load its + value from the ORM. The second argument to setValue() has been added. The following methods and properties on `Requirements_Backend` have been renamed: @@ -1320,6 +1333,7 @@ New `DatetimeField` methods replace `getConfig()` / `setConfig()`: * `getTimezone()` / `setTimezone()` * `getDateTimeOrder()` / `setDateTimeOrder()` * `getLocale()` / `setLocale()` +* `datavaluefield` config is removed as internal data value is now fixed to ISO 8601 format New `DateField` methods replace `getConfig()` / `setConfig()`: @@ -1328,9 +1342,9 @@ New `DateField` methods replace `getConfig()` / `setConfig()`: * `getMinDate()` / `setMinDate()` * `getMaxDate()` / `setMaxDate()` * `getPlaceholders()` / `setPlaceholders()` -* `getSeparateDMYFields()` / `setSeparateDMYFields()` * `getClientLocale` / `setClientLocale` * `getLocale()` / `setLocale()` +* option `dmyfields` is now superceded with an `SeparatedDateField` class New `TimeField` methods replace `getConfig()` / `setConfig()` @@ -1370,6 +1384,7 @@ New `TimeField` methods replace `getConfig()` / `setConfig()` as they are obsolete. * Removed `DatetimeField`, `DateField` and `TimeField` methods `getConfig` and `setConfig`. Individual getters and setters for individual options are provided instead. See above for list of new methods. +* Removed `NumericField_Readonly`. Use `setReadonly(true)` instead. ### i18n API @@ -1383,20 +1398,20 @@ New `TimeField` methods replace `getConfig()` / `setConfig()` for all DataObject subclasses, rather than just the basename without namespace. * i18n key for locale-respective pluralisation rules added as '.PLURALS'. These can be configured within yaml in array format as per [ruby i18n pluralization rules](http://guides.rubyonrails.org/i18n.html#pluralization). -* `i18n.all_locales` config moved to `Locales.locales` -* `i18n.common_languages` config moved to `Locales.languages` -* `i18n.likely_subtags` config moved to `Locales.likely_subtags` -* `i18n.tinymce_lang` config moved to `TinyMCEConfig.tinymce_lang` -* `i18n::get_tinymce_lang()` moved to `TinyMCEConfig::get_tinymce_lang()` -* `i18n::get_locale_from_lang()` moved to `Locales::localeFromLang()` -* `i18n::get_lange_from_locale()` moved to `Locales::langFromLocale()` -* `i18n::validate_locale()` moved to `Locales::validate()` -* `i18n::get_common_languages()` moved to `Locales::getLanguages()` -* `i18n::get_locale_name()` moved to `Locales::localeName()` -* `i18n::get_language_name()` moved to `Locales::languageName()` -* `i18n.module_priority` config moved to `Sources.module_priority` -* `i18n::get_owner_module()` moved to `ClassManifest::getOwnerModule()` -* `i18n::get_existing_translations()` moved to `Sources::getKnownLocales()` +* `i18n.all_locales` config moved to `SilverStripe\i18n\Data\Locales.locales` +* `i18n.common_languages` config moved to `SilverStripe\i18n\Data\Locales.languages` +* `i18n.likely_subtags` config moved to `SilverStripe\i18n\Data\Locales.likely_subtags` +* `i18n.tinymce_lang` config moved to `SilverStripe\Forms\HTMLEditor\TinyMCEConfig.tinymce_lang` +* `i18n::get_tinymce_lang()` moved to `SilverStripe\Forms\HTMLEditor\TinyMCEConfig::get_tinymce_lang()` +* `i18n::get_locale_from_lang()` moved to `SilverStripe\i18n\Data\Locales::localeFromLang()` +* `i18n::get_lange_from_locale()` moved to `SilverStripe\i18n\Data\Locales::langFromLocale()` +* `i18n::validate_locale()` moved to `SilverStripe\i18n\Data\Locales::validate()` +* `i18n::get_common_languages()` moved to `SilverStripe\i18n\Data\Locales::getLanguages()` +* `i18n::get_locale_name()` moved to `SilverStripe\i18n\Data\Locales::localeName()` +* `i18n::get_language_name()` moved to `SilverStripe\i18n\Data\Locales::languageName()` +* `i18n.module_priority` config moved to `SilverStripe\i18n\Data\Sources.module_priority` +* `i18n::get_owner_module()` moved to `SilverStripe\Core\Manifest\ClassManifest::getOwnerModule()` +* `i18n::get_existing_translations()` moved to `SilverStripe\i18n\Data\Sources::getKnownLocales()` #### i18n API Removed API diff --git a/src/Forms/DateField.php b/src/Forms/DateField.php index 8089a536034..6a24bad34c6 100644 --- a/src/Forms/DateField.php +++ b/src/Forms/DateField.php @@ -8,9 +8,7 @@ use SilverStripe\ORM\FieldType\DBDatetime; /** - * Form field to display an editable date string, - * either in a single `` field, - * or in three separate fields for day, month and year. + * Form used for editing a date stirng * * Caution: The form field does not include any JavaScript or CSS when used outside of the CMS context, * since the required frontend dependencies are included through CMS bundling. @@ -29,11 +27,10 @@ * * # Usage * - * ## Example: German dates with separate fields for day, month, year + * ## Example: Field localised with german date format * * $f = new DateField('MyDate'); * $f->setLocale('de_DE'); - * $f->setSeparateDMYFields(true); * * # Validation * @@ -95,14 +92,6 @@ class DateField extends TextField */ protected $placeholders = true; - /** - * Declare whether D, M and Y fields should be separate inputs. - * If set then only numeric values will be accepted. - * - * @var bool - */ - protected $separateDMYFields = false; - /** * Override locale for client side. * @@ -145,16 +134,11 @@ public function getShowCalendar() /** * Set if calendar should be shown on the frontend. * - * If set to true, disables separate DMY fields - * * @param bool $show * @return $this */ public function setShowCalendar($show) { - if ($show && $this->getSeparateDMYFields()) { - throw new InvalidArgumentException("Can't separate DMY fields and show calendar popup"); - } $this->showCalendar = $show; return $this; } @@ -316,39 +300,6 @@ public function getAttributes() return $attributes; } - public function Field($properties = array()) - { - if (!$this->getSeparateDMYFields()) { - return parent::Field($properties); - } - - // Three separate fields for day, month and year - $valArr = $this->iso8601ToArray($this->Value()); - $fieldDay = NumericField::create($this->name . '[day]', false, $valArr ? $valArr['day'] : null) - ->addExtraClass('day') - ->setMaxLength(2); - $fieldMonth = NumericField::create($this->name . '[month]', false, $valArr ? $valArr['month'] : null) - ->addExtraClass('month') - ->setMaxLength(2); - $fieldYear = NumericField::create($this->name . '[year]', false, $valArr ? $valArr['year'] : null) - ->addExtraClass('year') - ->setMaxLength(4); - - // Set placeholders - if ($this->getPlaceholders()) { - $fieldDay->setAttribute('placeholder', _t(__CLASS__ . '.DAY', 'Day')); - $fieldMonth->setAttribute('placeholder', _t(__CLASS__ . '.MONTH', 'Month')); - $fieldYear->setAttribute('placeholder', _t(__CLASS__ . '.YEAR', 'Year')); - } - - // Join all fields - // @todo custom ordering based on locale - $sep = ' / '; - return $fieldDay->Field() . $sep - . $fieldMonth->Field() . $sep - . $fieldYear->Field(); - } - public function Type() { return 'date text'; @@ -364,11 +315,7 @@ public function Type() public function setSubmittedValue($value, $data = null) { // Save raw value for later validation - if ($this->isEmptyArray($value)) { - $this->rawValue = null; - } else { - $this->rawValue = $value; - } + $this->rawValue = $value; // Null case if (!$value) { @@ -376,12 +323,6 @@ public function setSubmittedValue($value, $data = null) return $this; } - // If loading from array convert - if (is_array($value)) { - $this->value = $this->arrayToISO8601($value); - return $this; - } - // Parse from submitted value $this->value = $this->localisedToISO8601($value); return $this; @@ -555,33 +496,6 @@ public function setPlaceholders($placeholders) return $this; } - /** - * Declare whether D, M and Y fields should be separate inputs. - * If set then only numeric values will be accepted. - * - * @return bool - */ - public function getSeparateDMYFields() - { - return $this->separateDMYFields; - } - - /** - * Set if we should separate D M and Y fields. If set to true, disabled calendar - * popup. - * - * @param bool $separate - * @return $this - */ - public function setSeparateDMYFields($separate) - { - if ($separate && $this->getShowCalendar()) { - throw new InvalidArgumentException("Can't separate DMY fields and show calendar popup"); - } - $this->separateDMYFields = $separate; - return $this; - } - /** * @return string */ @@ -647,62 +561,6 @@ public function getClientConfig() return $config; } - /** - * Convert iso 8601 date to array (day / month / year) - * - * @param string $date - * @return array|null Array form, or null if not valid - */ - public function iso8601ToArray($date) - { - if (!$date) { - return null; - } - $formatter = $this->getISO8601Formatter(); - $timestamp = $formatter->parse($date); - if ($timestamp === false) { - return null; - } - - // Format time manually into an array - return [ - 'day' => date('j', $timestamp), - 'month' => date('n', $timestamp), - 'year' => date('Y', $timestamp), - ]; - } - - /** - * Convert array to timestamp - * - * @param array $value - * @return string - */ - public function arrayToISO8601($value) - { - if ($this->isEmptyArray($value)) { - return null; - } - - // ensure all keys are specified - if (!isset($value['month']) || !isset($value['day']) || !isset($value['year'])) { - return null; - } - - // Ensure valid range - if (!checkdate($value['month'], $value['day'], $value['year'])) { - return null; - } - - // Note: Set formatter to strict for array input - $formatter = $this->getISO8601Formatter(); - $timestamp = mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); - if ($timestamp === false) { - return null; - } - return $formatter->format($timestamp); - } - /** * Convert date localised in the current locale to ISO 8601 date * @@ -776,15 +634,4 @@ protected function getClientView() { return DateField_View_JQuery::create($this); } - - /** - * Check if this array is empty - * - * @param $value - * @return bool - */ - public function isEmptyArray($value) - { - return is_array($value) && !array_filter($value); - } } diff --git a/src/Forms/HTMLEditor/TinyMCEConfig.php b/src/Forms/HTMLEditor/TinyMCEConfig.php index 3fc83c33cc4..e51b68d6dfa 100644 --- a/src/Forms/HTMLEditor/TinyMCEConfig.php +++ b/src/Forms/HTMLEditor/TinyMCEConfig.php @@ -2,7 +2,6 @@ namespace SilverStripe\Forms\HTMLEditor; -use SilverStripe\Core\Config\Configurable; use SilverStripe\Core\Convert; use SilverStripe\Control\Controller; use SilverStripe\Control\Director; diff --git a/src/Forms/MemberDatetimeOptionsetField.php b/src/Forms/MemberDatetimeOptionsetField.php index 8e0913da7ca..e49b91e1ba1 100644 --- a/src/Forms/MemberDatetimeOptionsetField.php +++ b/src/Forms/MemberDatetimeOptionsetField.php @@ -159,7 +159,7 @@ public function setSubmittedValue($value, $data = null) public function setValue($value, $data = null) { if (is_array($value)) { - throw new InvalidArgumentException("Invalid value"); + throw new InvalidArgumentException("Invalid array value: Expected string"); } return parent::setValue($value, $data); } diff --git a/src/Forms/MoneyField.php b/src/Forms/MoneyField.php index 58fe123ad62..cfda105422c 100644 --- a/src/Forms/MoneyField.php +++ b/src/Forms/MoneyField.php @@ -89,7 +89,7 @@ protected function buildCurrencyField() $currencyValue = $this->fieldCurrency ? $this->fieldCurrency->dataValue() : null; $allowedCurrencies = $this->getAllowedCurrencies(); if (count($allowedCurrencies) === 1) { - // Dropdown field for multiple currencies + // Hidden field for single currency $field = HiddenField::create("{$name}[Currency]"); reset($allowedCurrencies); $currencyValue = key($allowedCurrencies); @@ -164,7 +164,7 @@ public function setValue($value, $data = null) 'Currency' => $value->getCurrency(), 'Amount' => $value->getAmount(), ]; - } else { + } elseif (!is_array($value)) { throw new InvalidArgumentException("Invalid currency format"); } @@ -232,8 +232,6 @@ public function saveInto(DataObjectInterface $dataObject) public function performReadonlyTransformation() { $clone = clone $this; - $clone->fieldAmount = $clone->fieldAmount->performReadonlyTransformation(); - $clone->fieldCurrency = $clone->fieldCurrency->performReadonlyTransformation(); $clone->setReadonly(true); return $clone; } diff --git a/src/Forms/NumericField.php b/src/Forms/NumericField.php index 122b149eef6..63a2007fbe6 100644 --- a/src/Forms/NumericField.php +++ b/src/Forms/NumericField.php @@ -306,4 +306,11 @@ public function setScale($scale) $this->scale = $scale; return $this; } + + public function performReadonlyTransformation() + { + $field = clone $this; + $field->setReadonly(true); + return $field; + } } diff --git a/src/Forms/NumericField_Readonly.php b/src/Forms/NumericField_Readonly.php deleted file mode 100644 index 52a80f64c2b..00000000000 --- a/src/Forms/NumericField_Readonly.php +++ /dev/null @@ -1,30 +0,0 @@ -value ?: '0'; - } - - public function getValueCast() - { - return 'Decimal'; - } -} diff --git a/src/Forms/SeparatedDateField.php b/src/Forms/SeparatedDateField.php new file mode 100644 index 00000000000..dc6999a957d --- /dev/null +++ b/src/Forms/SeparatedDateField.php @@ -0,0 +1,136 @@ +iso8601ToArray($this->dataValue()); + $fieldDay = NumericField::create($this->name . '[day]', false, $valArr ? $valArr['day'] : null) + ->addExtraClass('day') + ->setHTML5(true) + ->setMaxLength(2); + $fieldMonth = NumericField::create($this->name . '[month]', false, $valArr ? $valArr['month'] : null) + ->addExtraClass('month') + ->setHTML5(true) + ->setMaxLength(2); + $fieldYear = NumericField::create($this->name . '[year]', false, $valArr ? $valArr['year'] : null) + ->addExtraClass('year') + ->setHTML5(true) + ->setMaxLength(4); + + // Set placeholders + if ($this->getPlaceholders()) { + $fieldDay->setAttribute('placeholder', _t(__CLASS__ . '.DAY', 'Day')); + $fieldMonth->setAttribute('placeholder', _t(__CLASS__ . '.MONTH', 'Month')); + $fieldYear->setAttribute('placeholder', _t(__CLASS__ . '.YEAR', 'Year')); + } + + // Join all fields + // @todo custom ordering based on locale + $sep = ' / '; + return $fieldDay->Field() . $sep + . $fieldMonth->Field() . $sep + . $fieldYear->Field(); + } + + /** + * Convert array to timestamp + * + * @param array $value + * @return string + */ + public function arrayToISO8601($value) + { + if ($this->isEmptyArray($value)) { + return null; + } + + // ensure all keys are specified + if (!isset($value['month']) || !isset($value['day']) || !isset($value['year'])) { + return null; + } + + // Ensure valid range + if (!checkdate($value['month'], $value['day'], $value['year'])) { + return null; + } + + // Note: Set formatter to strict for array input + $formatter = $this->getISO8601Formatter(); + $timestamp = mktime(0, 0, 0, $value['month'], $value['day'], $value['year']); + if ($timestamp === false) { + return null; + } + return $formatter->format($timestamp); + } + + /** + * Convert iso 8601 date to array (day / month / year) + * + * @param string $date + * @return array|null Array form, or null if not valid + */ + public function iso8601ToArray($date) + { + if (!$date) { + return null; + } + $formatter = $this->getISO8601Formatter(); + $timestamp = $formatter->parse($date); + if ($timestamp === false) { + return null; + } + + // Format time manually into an array + return [ + 'day' => date('j', $timestamp), + 'month' => date('n', $timestamp), + 'year' => date('Y', $timestamp), + ]; + } + + /** + * Assign value posted from form submission + * + * @param mixed $value + * @param mixed $data + * @return $this + */ + public function setSubmittedValue($value, $data = null) + { + // Filter out empty arrays + if ($this->isEmptyArray($value)) { + $value = null; + } + $this->rawValue = $value; + + // Null case + if (!$value || !is_array($value)) { + $this->value = null; + return $this; + } + + // Parse + $this->value = $this->arrayToISO8601($value); + return $this; + } + + /** + * Check if this array is empty + * + * @param $value + * @return bool + */ + public function isEmptyArray($value) + { + return is_array($value) && !array_filter($value); + } +} diff --git a/src/ORM/FieldType/DBDate.php b/src/ORM/FieldType/DBDate.php index 461530acc6f..1e1fd26e8e3 100644 --- a/src/ORM/FieldType/DBDate.php +++ b/src/ORM/FieldType/DBDate.php @@ -38,7 +38,7 @@ public function setValue($value, $record = null, $markChanged = true) $value = $this->parseDate($value); if ($value === false) { throw new InvalidArgumentException( - "Invalid date passed. Use " . self::ISO_DATE . " to prevent this error." + "Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error." ); } $this->value = $value; @@ -308,10 +308,7 @@ public function Rfc822() */ public function Rfc2822() { - if ($this->value) { - return date('Y-m-d H:i:s', $this->getTimestamp()); - } - return null; + return $this->Format('y-MM-dd HH:mm:ss'); } /** @@ -321,15 +318,13 @@ public function Rfc2822() */ public function Rfc3339() { - if (!$this->value) { + $date = $this->Format('y-MM-dd\\THH:mm:ss'); + if (!$date) { return null; } - $timestamp = $this->getTimestamp(); - $date = date('Y-m-d\TH:i:s', $timestamp); - $matches = array(); - if (preg_match('/^([\-+])(\d{2})(\d{2})$/', date('O', $timestamp), $matches)) { + if (preg_match('/^([\-+])(\d{2})(\d{2})$/', date('O', $this->getTimestamp()), $matches)) { $date .= $matches[1].$matches[2].':'.$matches[3]; } else { $date .= 'Z'; @@ -541,23 +536,13 @@ public function scaffoldFormField($title = null, $params = null) protected function fixInputDate($value) { // split - list($day, $month, $year, $time) = $this->explodeDateString($value); - - // Detect invalid year order - if (!checkdate($month, $day, $year) && checkdate($month, $year, $day)) { - trigger_error( - "Unexpected date order. Use " . self::ISO_DATE . " to prevent this notice.", - E_USER_NOTICE - ); - list($day, $year) = [$year, $day]; - } - - // Fix y2k year - $year = $this->guessY2kYear($year); + list($year, $month, $day, $time) = $this->explodeDateString($value); // Validate date if (!checkdate($month, $day, $year)) { - throw new InvalidArgumentException("Invalid date passed. Use " . self::ISO_DATE . " to prevent this error."); + throw new InvalidArgumentException( + "Invalid date: '$value'. Use " . self::ISO_DATE . " to prevent this error." + ); } // Convert to y-m-d @@ -565,65 +550,39 @@ protected function fixInputDate($value) } /** - * Attempt to split date string into day, month, year, and timestamp components. - * Don't read this code without a drink in hand! + * Attempt to split date string into year, month, day, and timestamp components. * * @param string $value * @return array */ protected function explodeDateString($value) { - // US date format with 4-digit year first - if (preg_match('#^(?\\d{4})/(?\\d+)/(?\\d+)(?