374 lines
12 KiB
Dart
374 lines
12 KiB
Dart
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
final DateTime _kDefaultFirstSelectableDate = DateTime(1900);
|
|
final DateTime _kDefaultLastSelectableDate = DateTime(2100);
|
|
|
|
const double kCupertinoDatePickerHeight = 216;
|
|
|
|
/// Constructor tearoff definition that matches [DateTimeField.new]
|
|
// Note: This should match the definition of the [DateTimeField] constructor
|
|
typedef DateTimeFieldCreator = DateTimeField Function({
|
|
Key? key,
|
|
required ValueChanged<DateTime>? onDateSelected,
|
|
required DateTime? selectedDate,
|
|
DateFormat? dateFormat,
|
|
TextStyle? dateTextStyle,
|
|
InputDecoration? decoration,
|
|
bool? enabled,
|
|
DateTime? firstDate,
|
|
DateTime? initialDate,
|
|
DatePickerMode? initialDatePickerMode,
|
|
DatePickerEntryMode initialEntryMode,
|
|
DateTime? lastDate,
|
|
DateTimeFieldPickerMode mode,
|
|
bool use24hFormat,
|
|
TimePickerEntryMode initialTimePickerEntryMode,
|
|
});
|
|
|
|
/// [DateTimeField]
|
|
///
|
|
/// Shows an [_InputDropdown] that'll trigger [DateTimeField._selectDate] whenever the user
|
|
/// clicks on it ! The date picker is **platform responsive** (ios date picker style for ios, ...)
|
|
class DateTimeField extends StatelessWidget {
|
|
// Note: This should match the definition of the [DateTimeFieldCreator]
|
|
DateTimeField({
|
|
Key? key,
|
|
required this.onDateSelected,
|
|
required this.selectedDate,
|
|
this.initialDatePickerMode = DatePickerMode.day,
|
|
this.decoration,
|
|
this.enabled = true,
|
|
this.mode = DateTimeFieldPickerMode.dateAndTime,
|
|
this.initialEntryMode = DatePickerEntryMode.calendar,
|
|
this.dateTextStyle,
|
|
this.initialDate,
|
|
this.use24hFormat = false,
|
|
DateTime? firstDate,
|
|
DateTime? lastDate,
|
|
DateFormat? dateFormat,
|
|
this.initialTimePickerEntryMode = TimePickerEntryMode.dial,
|
|
}) : dateFormat = dateFormat ?? getDateFormatFromDateFieldPickerMode(mode),
|
|
firstDate = firstDate ?? _kDefaultFirstSelectableDate,
|
|
lastDate = lastDate ?? _kDefaultLastSelectableDate,
|
|
super(key: key);
|
|
|
|
DateTimeField.time({
|
|
Key? key,
|
|
this.onDateSelected,
|
|
this.selectedDate,
|
|
this.decoration,
|
|
this.enabled,
|
|
this.initialDate,
|
|
this.dateTextStyle,
|
|
this.use24hFormat = false,
|
|
this.initialEntryMode = DatePickerEntryMode.calendar,
|
|
this.initialTimePickerEntryMode = TimePickerEntryMode.dial,
|
|
DateTime? firstDate,
|
|
DateTime? lastDate,
|
|
}) : initialDatePickerMode = null,
|
|
mode = DateTimeFieldPickerMode.time,
|
|
dateFormat = DateFormat.jm(),
|
|
firstDate = firstDate ?? DateTime(2000),
|
|
lastDate = lastDate ?? DateTime(2001),
|
|
super(key: key);
|
|
|
|
/// Callback for whenever the user selects a [DateTime]
|
|
final ValueChanged<DateTime>? onDateSelected;
|
|
|
|
/// The current selected date to display inside the field
|
|
final DateTime? selectedDate;
|
|
|
|
/// The first date that the user can select (default is 1900)
|
|
final DateTime firstDate;
|
|
|
|
/// The last date that the user can select (default is 2100)
|
|
final DateTime lastDate;
|
|
|
|
/// The date that will be selected by default in the calendar view.
|
|
final DateTime? initialDate;
|
|
|
|
/// Let you choose the [DatePickerMode] for the date picker! (default is [DatePickerMode.day]
|
|
final DatePickerMode? initialDatePickerMode;
|
|
|
|
/// Custom [InputDecoration] for the [InputDecorator] widget
|
|
final InputDecoration? decoration;
|
|
|
|
/// How to display the [DateTime] for the user (default is [DateFormat.yMMMD])
|
|
final DateFormat dateFormat;
|
|
|
|
/// Whether the field is usable. If false the user won't be able to select any date
|
|
final bool? enabled;
|
|
|
|
/// Whether to use the 24Hr Format
|
|
final bool use24hFormat;
|
|
|
|
/// Whether to ask the user to pick only the date, the time or both.
|
|
final DateTimeFieldPickerMode mode;
|
|
|
|
/// [TextStyle] of the selected date inside the field.
|
|
final TextStyle? dateTextStyle;
|
|
|
|
/// The initial entry mode for the material date picker dialog
|
|
final DatePickerEntryMode initialEntryMode;
|
|
|
|
// the initial entry mode for the material time picker dialog
|
|
final TimePickerEntryMode initialTimePickerEntryMode;
|
|
|
|
/// Shows a dialog asking the user to pick a date !
|
|
Future<void> _selectDate(BuildContext context) async {
|
|
final DateTime initialDateTime;
|
|
|
|
if (selectedDate != null) {
|
|
initialDateTime = selectedDate!;
|
|
} else {
|
|
final DateTime now = DateTime.now();
|
|
if (firstDate.isAfter(now) || lastDate.isBefore(now)) {
|
|
initialDateTime = initialDate ?? lastDate;
|
|
} else {
|
|
initialDateTime = initialDate ?? now;
|
|
}
|
|
}
|
|
|
|
if (Theme.of(context).platform == TargetPlatform.iOS) {
|
|
final DateTime? _selectedDateTime = await showCupertinoPicker(context, initialDateTime);
|
|
if (_selectedDateTime != null) {
|
|
onDateSelected!(_selectedDateTime);
|
|
}
|
|
} else {
|
|
DateTime _selectedDateTime = initialDateTime;
|
|
|
|
const List<DateTimeFieldPickerMode> modesWithDate = <DateTimeFieldPickerMode>[
|
|
DateTimeFieldPickerMode.dateAndTime,
|
|
DateTimeFieldPickerMode.date
|
|
];
|
|
|
|
if (modesWithDate.contains(mode)) {
|
|
final DateTime? _selectedDate = await showMaterialDatePicker(context, initialDateTime);
|
|
|
|
if (_selectedDate != null) {
|
|
_selectedDateTime = _selectedDate;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
final List<DateTimeFieldPickerMode> modesWithTime = <DateTimeFieldPickerMode>[
|
|
DateTimeFieldPickerMode.dateAndTime,
|
|
DateTimeFieldPickerMode.time
|
|
];
|
|
|
|
if (modesWithTime.contains(mode)) {
|
|
final TimeOfDay? _selectedTime = await showMaterialTimePicker(context, initialDateTime,
|
|
initialEntryMode: initialTimePickerEntryMode);
|
|
|
|
if (_selectedTime != null) {
|
|
_selectedDateTime = DateTime(
|
|
_selectedDateTime.year,
|
|
_selectedDateTime.month,
|
|
_selectedDateTime.day,
|
|
_selectedTime.hour,
|
|
_selectedTime.minute,
|
|
);
|
|
}
|
|
}
|
|
|
|
onDateSelected!(_selectedDateTime);
|
|
}
|
|
}
|
|
|
|
/// Launches the Material time picker by invoking [showTimePicker].
|
|
/// Can be @[override]n to allow further customization of the picker options
|
|
Future<TimeOfDay?> showMaterialTimePicker(BuildContext context, DateTime initialDateTime,
|
|
{TimePickerEntryMode initialEntryMode = TimePickerEntryMode.dial}) async {
|
|
return showTimePicker(
|
|
initialTime: TimeOfDay.fromDateTime(initialDateTime),
|
|
context: context,
|
|
initialEntryMode: initialEntryMode,
|
|
builder: (BuildContext context, Widget? child) {
|
|
return MediaQuery(
|
|
data: MediaQuery.of(context).copyWith(
|
|
alwaysUse24HourFormat: use24hFormat,
|
|
),
|
|
child: child!,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Launches the Material time picker by invoking [showDatePicker].
|
|
/// Can be @[override]n to allow further customization of the picker options
|
|
Future<DateTime?> showMaterialDatePicker(
|
|
BuildContext context,
|
|
DateTime initialDateTime,
|
|
) {
|
|
return showDatePicker(
|
|
context: context,
|
|
initialDatePickerMode: initialDatePickerMode!,
|
|
initialDate: initialDateTime,
|
|
initialEntryMode: initialEntryMode,
|
|
firstDate: firstDate,
|
|
lastDate: lastDate,
|
|
);
|
|
}
|
|
|
|
/// Launches the [CupertinoDatePicker] within a [showModalBottomSheet].
|
|
/// Can be @[override]n to allow further customization of the picker options
|
|
Future<DateTime?> showCupertinoPicker(
|
|
BuildContext context,
|
|
DateTime initialDateTime,
|
|
) async {
|
|
DateTime? pickedDate;
|
|
await showModalBottomSheet<DateTime?>(
|
|
context: context,
|
|
builder: (BuildContext builder) {
|
|
return SizedBox(
|
|
height: kCupertinoDatePickerHeight,
|
|
child: CupertinoDatePicker(
|
|
mode: cupertinoModeFromPickerMode(mode),
|
|
onDateTimeChanged: (DateTime dt) => pickedDate = dt,
|
|
initialDateTime: initialDateTime,
|
|
minimumDate: firstDate,
|
|
maximumDate: lastDate,
|
|
use24hFormat: use24hFormat,
|
|
),
|
|
);
|
|
},
|
|
);
|
|
return pickedDate;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
String? text;
|
|
|
|
if (selectedDate != null) {
|
|
text = dateFormat.format(selectedDate!);
|
|
}
|
|
TextStyle? textStyle;
|
|
|
|
textStyle = dateTextStyle ?? dateTextStyle;
|
|
|
|
return _InputDropdown(
|
|
text: text,
|
|
textStyle: textStyle,
|
|
isEmpty: selectedDate == null,
|
|
decoration: decoration,
|
|
onPressed: enabled! ? () => _selectDate(context) : null,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Those values are used by the [DateTimeField] widget to determine whether to ask
|
|
/// the user for the time, the date or both.
|
|
enum DateTimeFieldPickerMode { time, date, dateAndTime }
|
|
|
|
/// Returns the [CupertinoDatePickerMode] corresponding to the selected
|
|
/// [DateTimeFieldPickerMode]. This exists to prevent redundancy in the [DateTimeField]
|
|
/// widget parameters.
|
|
CupertinoDatePickerMode cupertinoModeFromPickerMode(DateTimeFieldPickerMode mode) {
|
|
switch (mode) {
|
|
case DateTimeFieldPickerMode.time:
|
|
return CupertinoDatePickerMode.time;
|
|
case DateTimeFieldPickerMode.date:
|
|
return CupertinoDatePickerMode.date;
|
|
default:
|
|
return CupertinoDatePickerMode.dateAndTime;
|
|
}
|
|
}
|
|
|
|
/// Returns the corresponding default [DateFormat] for the selected [DateTimeFieldPickerMode]
|
|
DateFormat getDateFormatFromDateFieldPickerMode(DateTimeFieldPickerMode mode) {
|
|
switch (mode) {
|
|
case DateTimeFieldPickerMode.time:
|
|
return DateFormat.jm();
|
|
case DateTimeFieldPickerMode.date:
|
|
return DateFormat.yMMMMd();
|
|
default:
|
|
return DateFormat.yMd().add_jm();
|
|
}
|
|
}
|
|
|
|
///
|
|
/// [_InputDropdown]
|
|
///
|
|
/// Shows a field with a dropdown arrow !
|
|
/// It does not show any popup menu, it'll just trigger onPressed whenever the
|
|
/// user does click on it !
|
|
class _InputDropdown extends StatefulWidget {
|
|
const _InputDropdown({
|
|
Key? key,
|
|
required this.text,
|
|
this.decoration,
|
|
this.textStyle,
|
|
this.onPressed,
|
|
required this.isEmpty,
|
|
}) : super(key: key);
|
|
|
|
/// The text that should be displayed inside the field
|
|
final String? text;
|
|
|
|
/// Custom [InputDecoration] for the [InputDecorator] widget
|
|
final InputDecoration? decoration;
|
|
|
|
/// TextStyle for the field
|
|
final TextStyle? textStyle;
|
|
|
|
/// Callbacks triggered whenever the user presses on the field!
|
|
final VoidCallback? onPressed;
|
|
|
|
/// Whether the input field is empty.
|
|
///
|
|
/// Determines the position of the label text and whether to display the hint
|
|
/// text.
|
|
///
|
|
/// Defaults to false.
|
|
final bool isEmpty;
|
|
|
|
@override
|
|
State<_InputDropdown> createState() => _InputDropdownState();
|
|
}
|
|
|
|
class _InputDropdownState extends State<_InputDropdown> {
|
|
bool focused = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final InputDecoration effectiveDecoration = widget.decoration ??
|
|
const InputDecoration(
|
|
suffixIcon: Icon(Icons.arrow_drop_down),
|
|
);
|
|
|
|
return MouseRegion(
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: widget.onPressed,
|
|
child: Focus(
|
|
onFocusChange: (bool newFocus) => setState(() {
|
|
focused = newFocus;
|
|
}),
|
|
onKey: (_, RawKeyEvent key) {
|
|
if (key.isKeyPressed(LogicalKeyboardKey.space)) {
|
|
widget.onPressed?.call();
|
|
return KeyEventResult.handled;
|
|
}
|
|
return KeyEventResult.ignored;
|
|
},
|
|
child: InputDecorator(
|
|
isHovering: focused,
|
|
decoration: effectiveDecoration.applyDefaults(
|
|
Theme.of(context).inputDecorationTheme,
|
|
),
|
|
isEmpty: widget.isEmpty,
|
|
child: widget.text == null
|
|
? Text('', style: widget.textStyle)
|
|
: Text(widget.text!, style: widget.textStyle),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|