2007-07-19 12:40:28 +02:00
|
|
|
<?php
|
2013-05-11 12:51:39 +02:00
|
|
|
|
2016-08-19 00:51:35 +02:00
|
|
|
namespace SilverStripe\Forms;
|
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
use NumberFormatter;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\i18n\i18n;
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
2013-05-11 12:51:39 +02:00
|
|
|
* Text input field with validation for numeric values. Supports validating
|
2014-04-02 22:33:18 +02:00
|
|
|
* the numeric value as to the {@link i18n::get_locale()} value, or an
|
|
|
|
* overridden locale specific to this field.
|
2007-07-19 12:40:28 +02:00
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class NumericField extends TextField
|
|
|
|
{
|
|
|
|
|
|
|
|
protected $schemaDataType = FormField::SCHEMA_DATA_TYPE_DECIMAL;
|
|
|
|
|
2017-05-08 07:21:51 +02:00
|
|
|
protected $inputType = 'number';
|
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
/**
|
|
|
|
* Used to determine if the number given is in the correct format when validating
|
|
|
|
*
|
|
|
|
* @var mixed
|
|
|
|
*/
|
|
|
|
protected $originalValue = null;
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Override locale for this field.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $locale = null;
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Use HTML5 number input type.
|
|
|
|
* Note that enabling html5 disables certain localisation features.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected $html5 = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Number of decimal places allowed, if bound.
|
|
|
|
* Null means unbound.
|
|
|
|
* Defaults to 0, which is integer value.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
protected $scale = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get number formatter for localising this field
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @return NumberFormatter
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
protected function getFormatter()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
if ($this->getHTML5()) {
|
|
|
|
// Locale-independent html5 number formatter
|
2017-02-22 04:14:53 +01:00
|
|
|
$formatter = NumberFormatter::create(
|
|
|
|
i18n::config()->uninherited('default_locale'),
|
|
|
|
NumberFormatter::DECIMAL
|
|
|
|
);
|
2017-01-26 05:20:08 +01:00
|
|
|
$formatter->setAttribute(NumberFormatter::GROUPING_USED, false);
|
|
|
|
$formatter->setSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL, '.');
|
|
|
|
} else {
|
|
|
|
// Locale-specific number formatter
|
|
|
|
$formatter = NumberFormatter::create($this->getLocale(), NumberFormatter::DECIMAL);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
// Set decimal precision
|
|
|
|
$scale = $this->getScale();
|
|
|
|
if ($scale === 0) {
|
|
|
|
$formatter->setAttribute(NumberFormatter::DECIMAL_ALWAYS_SHOWN, false);
|
|
|
|
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, 0);
|
|
|
|
} else {
|
|
|
|
$formatter->setAttribute(NumberFormatter::DECIMAL_ALWAYS_SHOWN, true);
|
|
|
|
if ($scale === null) {
|
|
|
|
// At least one digit to distinguish floating point from integer
|
|
|
|
$formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 1);
|
|
|
|
} else {
|
|
|
|
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $scale);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $formatter;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
/**
|
|
|
|
* Get type argument for parse / format calls. one of TYPE_INT32, TYPE_INT64 or TYPE_DOUBLE
|
|
|
|
*
|
|
|
|
* @return int
|
|
|
|
*/
|
|
|
|
protected function getNumberType()
|
|
|
|
{
|
|
|
|
$scale = $this->getScale();
|
|
|
|
if ($scale === 0) {
|
|
|
|
return PHP_INT_SIZE > 4
|
|
|
|
? NumberFormatter::TYPE_INT64
|
|
|
|
: NumberFormatter::TYPE_INT32;
|
|
|
|
}
|
|
|
|
return NumberFormatter::TYPE_DOUBLE;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
public function setSubmittedValue($value, $data = null)
|
|
|
|
{
|
|
|
|
// Save original value in case parse fails
|
|
|
|
$value = trim($value);
|
|
|
|
$this->originalValue = $value;
|
|
|
|
|
|
|
|
// Empty string is no-number (not 0)
|
|
|
|
if (strlen($value) === 0) {
|
|
|
|
$this->value = null;
|
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
// Format number
|
|
|
|
$formatter = $this->getFormatter();
|
|
|
|
$parsed = 0;
|
|
|
|
$this->value = $formatter->parse($value, $this->getNumberType(), $parsed); // Note: may store literal `false` for invalid values
|
|
|
|
// Ensure that entire string is parsed
|
|
|
|
if ($parsed < strlen($value)) {
|
|
|
|
$this->value = false;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Format value for output
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
public function Value()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
// Show invalid value back to user in case of error
|
2017-07-04 21:35:13 +02:00
|
|
|
if ($this->value === null || $this->value === false) {
|
2017-01-26 05:20:08 +01:00
|
|
|
return $this->originalValue;
|
|
|
|
}
|
|
|
|
$formatter = $this->getFormatter();
|
|
|
|
return $formatter->format($this->value, $this->getNumberType());
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
public function setValue($value, $data = null)
|
|
|
|
{
|
|
|
|
$this->originalValue = $value;
|
|
|
|
$this->value = $this->cast($value);
|
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Helper to cast non-localised strings to their native type
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @param string $value
|
|
|
|
* @return float|int
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
protected function cast($value)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-07-04 21:35:13 +02:00
|
|
|
if (strlen($value) === 0) {
|
|
|
|
return null;
|
|
|
|
}
|
2017-01-26 05:20:08 +01:00
|
|
|
if ($this->getScale() === 0) {
|
|
|
|
return (int)$value;
|
|
|
|
}
|
|
|
|
return (float)$value;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function Type()
|
|
|
|
{
|
|
|
|
return 'numeric text';
|
|
|
|
}
|
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
public function getAttributes()
|
|
|
|
{
|
|
|
|
$attributes = parent::getAttributes();
|
|
|
|
if ($this->getHTML5()) {
|
|
|
|
$attributes['step'] = $this->getStep();
|
2017-05-08 07:21:51 +02:00
|
|
|
} else {
|
|
|
|
$attributes['type'] = 'text';
|
2017-01-26 05:20:08 +01:00
|
|
|
}
|
2017-05-08 07:21:51 +02:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
return $attributes;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Validate this field
|
|
|
|
*
|
|
|
|
* @param Validator $validator
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public function validate($validator)
|
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
// false signifies invalid value due to failed parse()
|
|
|
|
if ($this->value !== false) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
$validator->validationError(
|
|
|
|
$this->name,
|
|
|
|
_t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Forms\\NumericField.VALIDATION',
|
2016-11-29 00:31:16 +01:00
|
|
|
"'{value}' is not a number, only numbers can be accepted for this field",
|
2017-01-26 05:20:08 +01:00
|
|
|
['value' => $this->originalValue]
|
|
|
|
)
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getSchemaValidation()
|
|
|
|
{
|
|
|
|
$rules = parent::getSchemaValidation();
|
|
|
|
$rules['numeric'] = true;
|
|
|
|
return $rules;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Get internal database value
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @return int|float
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function dataValue()
|
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
return $this->cast($this->value);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
/**
|
|
|
|
* Gets the current locale this field is set to.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public function getLocale()
|
|
|
|
{
|
|
|
|
if ($this->locale) {
|
|
|
|
return $this->locale;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
return i18n::get_locale();
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
/**
|
|
|
|
* Override the locale for this field.
|
|
|
|
*
|
|
|
|
* @param string $locale
|
|
|
|
*
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setLocale($locale)
|
|
|
|
{
|
|
|
|
$this->locale = $locale;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Determine if we should use html5 number input
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @return bool
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
public function getHTML5()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
return $this->html5;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-01-26 05:20:08 +01:00
|
|
|
/**
|
|
|
|
* Set whether this field should use html5 number input type.
|
|
|
|
* Note: If setting to true this will disable all number localisation.
|
|
|
|
*
|
|
|
|
* @param bool $html5
|
|
|
|
* @return $this
|
|
|
|
*/
|
|
|
|
public function setHTML5($html5)
|
|
|
|
{
|
|
|
|
$this->html5 = $html5;
|
|
|
|
return $this;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Step attribute for html5. E.g. '0.01' to enable two decimal places.
|
|
|
|
* Ignored if html5 isn't enabled.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
public function getStep()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
$scale = $this->getScale();
|
|
|
|
if ($scale === null) {
|
|
|
|
return 'any';
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-01-26 05:20:08 +01:00
|
|
|
if ($scale === 0) {
|
|
|
|
return '1';
|
|
|
|
}
|
2018-01-16 19:39:30 +01:00
|
|
|
return '0.' . str_repeat('0', $scale - 1) . '1';
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-01-26 05:20:08 +01:00
|
|
|
* Get number of digits to show to the right of the decimal point.
|
|
|
|
* 0 for integer, any number for floating point, or null to flexible
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @return int|null
|
|
|
|
*/
|
|
|
|
public function getScale()
|
|
|
|
{
|
|
|
|
return $this->scale;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get number of digits to show to the right of the decimal point.
|
|
|
|
* 0 for integer, any number for floating point, or null to flexible
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-01-26 05:20:08 +01:00
|
|
|
* @param int|null $scale
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return $this
|
|
|
|
*/
|
2017-01-26 05:20:08 +01:00
|
|
|
public function setScale($scale)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-01-26 05:20:08 +01:00
|
|
|
$this->scale = $scale;
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
2017-02-14 06:19:09 +01:00
|
|
|
|
|
|
|
public function performReadonlyTransformation()
|
|
|
|
{
|
|
|
|
$field = clone $this;
|
|
|
|
$field->setReadonly(true);
|
|
|
|
return $field;
|
|
|
|
}
|
2007-07-19 12:40:28 +02:00
|
|
|
}
|