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? 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? 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 _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 modesWithDate = [ DateTimeFieldPickerMode.dateAndTime, DateTimeFieldPickerMode.date ]; if (modesWithDate.contains(mode)) { final DateTime? _selectedDate = await showMaterialDatePicker(context, initialDateTime); if (_selectedDate != null) { _selectedDateTime = _selectedDate; } else { return; } } final List modesWithTime = [ 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 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 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 showCupertinoPicker( BuildContext context, DateTime initialDateTime, ) async { DateTime? pickedDate; await showModalBottomSheet( 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), ), ), ), ); } }