blob: dda971425157a783005ca633a52b9aab05361789 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import '../input_border.dart';
import '../input_decorator.dart';
import '../material_localizations.dart';
import '../text_form_field.dart';
import '../theme.dart';
import 'date_picker_common.dart';
import 'date_utils.dart' as utils;
/// A [TextFormField] configured to accept and validate a date entered by the user.
///
/// The text entered into this field will be constrained to only allow digits
/// and separators. When saved or submitted, the text will be parsed into a
/// [DateTime] according to the ambient locale. If the input text doesn't parse
/// into a date, the [errorFormatText] message will be displayed under the field.
///
/// [firstDate], [lastDate], and [selectableDayPredicate] provide constraints on
/// what days are valid. If the input date isn't in the date range or doesn't pass
/// the given predicate, then the [errorInvalidText] message will be displayed
/// under the field.
///
/// See also:
///
/// * [showDatePicker], which shows a dialog that contains a material design
/// date picker which includes support for text entry of dates.
/// * [MaterialLocalizations.parseCompactDate], which is used to parse the text
/// input into a [DateTime].
///
class InputDatePickerFormField extends StatefulWidget {
/// Creates a [TextFormField] configured to accept and validate a date.
///
/// If the optional [initialDate] is provided, then it will be used to populate
/// the text field. If the [fieldHintText] is provided, it will be shown.
///
/// If [initialDate] is provided, it must not be before [firstDate] or after
/// [lastDate]. If [selectableDayPredicate] is provided, it must return `true`
/// for [initialDate].
///
/// [firstDate] must be on or before [lastDate].
///
/// [firstDate], [lastDate], and [autofocus] must be non-null.
///
InputDatePickerFormField({
Key key,
DateTime initialDate,
@required DateTime firstDate,
@required DateTime lastDate,
this.onDateSubmitted,
this.onDateSaved,
this.selectableDayPredicate,
this.errorFormatText,
this.errorInvalidText,
this.fieldHintText,
this.fieldLabelText,
this.autofocus = false,
}) : assert(firstDate != null),
assert(lastDate != null),
assert(autofocus != null),
initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
firstDate = utils.dateOnly(firstDate),
lastDate = utils.dateOnly(lastDate),
super(key: key) {
assert(
!this.lastDate.isBefore(this.firstDate),
'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isBefore(this.firstDate),
'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
);
assert(
initialDate == null || !this.initialDate.isAfter(this.lastDate),
'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
);
assert(
selectableDayPredicate == null || initialDate == null || selectableDayPredicate(this.initialDate),
'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.'
);
}
/// If provided, it will be used as the default value of the field.
final DateTime initialDate;
/// The earliest allowable [DateTime] that the user can input.
final DateTime firstDate;
/// The latest allowable [DateTime] that the user can input.
final DateTime lastDate;
/// An optional method to call when the user indicates they are done editing
/// the text in the field. Will only be called if the input represents a valid
/// [DateTime].
final ValueChanged<DateTime> onDateSubmitted;
/// An optional method to call with the final date when the form is
/// saved via [FormState.save]. Will only be called if the input represents
/// a valid [DateTime].
final ValueChanged<DateTime> onDateSaved;
/// Function to provide full control over which [DateTime] can be selected.
final SelectableDayPredicate selectableDayPredicate;
/// The error text displayed if the entered date is not in the correct format.
final String errorFormatText;
/// The error text displayed if the date is not valid.
///
/// A date is not valid if it is earlier than [firstDate], later than
/// [lastDate], or doesn't pass the [selectableDayPredicate].
final String errorInvalidText;
/// The hint text displayed in the [TextField].
///
/// If this is null, it will default to the date format string. For example,
/// 'mm/dd/yyyy' for en_US.
final String fieldHintText;
/// The label text displayed in the [TextField].
///
/// If this is null, it will default to the words representing the date format
/// string. For example, 'Month, Day, Year' for en_US.
final String fieldLabelText;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
@override
_InputDatePickerFormFieldState createState() => _InputDatePickerFormFieldState();
}
class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
final TextEditingController _controller = TextEditingController();
DateTime _selectedDate;
String _inputText;
bool _autoSelected = false;
@override
void initState() {
super.initState();
_selectedDate = widget.initialDate;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_selectedDate != null) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
_inputText = localizations.formatCompactDate(_selectedDate);
TextEditingValue textEditingValue = _controller.value.copyWith(text: _inputText);
// Select the new text if we are auto focused and haven't selected the text before.
if (widget.autofocus && !_autoSelected) {
textEditingValue = textEditingValue.copyWith(selection: TextSelection(
baseOffset: 0,
extentOffset: _inputText.length,
));
_autoSelected = true;
}
_controller.value = textEditingValue;
}
}
DateTime _parseDate(String text) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
return localizations.parseCompactDate(text);
}
bool _isValidAcceptableDate(DateTime date) {
return
date != null &&
!date.isBefore(widget.firstDate) &&
!date.isAfter(widget.lastDate) &&
(widget.selectableDayPredicate == null || widget.selectableDayPredicate(date));
}
String _validateDate(String text) {
final DateTime date = _parseDate(text);
if (date == null) {
return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel;
} else if (!_isValidAcceptableDate(date)) {
return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel;
}
return null;
}
void _handleSaved(String text) {
if (widget.onDateSaved != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSaved(date);
}
}
}
void _handleSubmitted(String text) {
if (widget.onDateSubmitted != null) {
final DateTime date = _parseDate(text);
if (_isValidAcceptableDate(date)) {
_selectedDate = date;
_inputText = text;
widget.onDateSubmitted(date);
}
}
}
@override
Widget build(BuildContext context) {
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
return TextFormField(
decoration: InputDecoration(
border: inputTheme.border ?? const UnderlineInputBorder(),
filled: inputTheme.filled ?? true,
hintText: widget.fieldHintText ?? localizations.dateHelpText,
labelText: widget.fieldLabelText ?? localizations.dateInputLabel,
),
validator: _validateDate,
inputFormatters: <TextInputFormatter>[
DateTextInputFormatter(localizations.dateSeparator),
],
keyboardType: TextInputType.datetime,
onSaved: _handleSaved,
onFieldSubmitted: _handleSubmitted,
autofocus: widget.autofocus,
controller: _controller,
);
}
}
/// A `TextInputFormatter` set up to format dates.
//
// This is not publicly exported (see pickers.dart), as it is
// just meant for internal use by `InputDatePickerFormField` and
// `InputDateRangePicker`.
class DateTextInputFormatter extends TextInputFormatter {
/// Creates a date formatter with the given separator.
DateTextInputFormatter(
this.separator
) : _filterFormatter = FilteringTextInputFormatter.allow(RegExp('[\\d$_commonSeparators\\$separator]+'));
/// List of common separators that are used in dates. This is used to make
/// sure that if given platform's [TextInputType.datetime] keyboard doesn't
/// provide the given locale's separator character, they can still enter the
/// separator using one of these characters (slash, period, comma, dash, or
/// space).
static const String _commonSeparators = r'\/\.,-\s';
/// The date separator for the current locale.
final String separator;
// Formatter that will filter out all characters except digits and date
// separators.
final TextInputFormatter _filterFormatter;
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final TextEditingValue filteredValue = _filterFormatter.formatEditUpdate(oldValue, newValue);
return filteredValue.copyWith(
// Replace any non-digits with the given separator
text: filteredValue.text.replaceAll(RegExp(r'[\D]'), separator),
);
}
}