diff --git a/CHANGELOG.md b/CHANGELOG.md index 7588c40..85e197d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +##2.1.0 + +* Fixing label & hint style issues +* Moving to a more generic architecture +* Updating the analysis_options.yaml file + ##2.0.1 * Adding the ability to specify the entry mode for the material date picker. diff --git a/README.md b/README.md index e092b7d..77828fe 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ In the `pubspec.yaml` of your flutter project, add the following dependency: ```yaml dependencies: ... - date_field: ^2.0.1 + date_field: ^2.1.0 ``` In your library add the following import: @@ -63,4 +63,4 @@ DateTimeFormField( ), ``` -You can check the Github repo for a complete example. \ No newline at end of file +You can check the GitHub repo for a complete example. \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 17bc5a5..5eda6fa 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,43 +1,14 @@ -# Specify analysis options. -# -# Until there are meta linter rules, each desired lint must be explicitly enabled. -# See: https://github.com/dart-lang/linter/issues/288 -# -# For a list of lints, see: http://dart-lang.github.io/linter/lints/ -# See the configuration guide for more -# https://github.com/dart-lang/sdk/tree/master/pkg/analyzer#configuring-the-analyzer -# -# There are other similar analysis options files in the flutter repos, -# which should be kept in sync with this file: -# -# - analysis_options.yaml (this file) -# - packages/flutter/lib/analysis_options_user.yaml -# - https://github.com/flutter/plugins/blob/master/analysis_options.yaml -# - https://github.com/flutter/engine/blob/master/analysis_options.yaml -# -# This file contains the analysis options used by Flutter tools, such as IntelliJ, -# Android Studio, and the `flutter analyze` command. - analyzer: strong-mode: + implicit-casts: false implicit-dynamic: false errors: - # treat missing required parameters as a warning (not a hint) missing_required_param: warning - # treat missing returns as a warning (not a hint) missing_return: warning - # allow having TODOs in the code - todo: ignore - # Ignore analyzer hints for updating pubspecs when using Future or - # Stream and not importing dart:async - # Please see https://github.com/flutter/flutter/pull/24528 for details. - sdk_version_async_exported_from_core: ignore exclude: - "bin/cache/**" - # the following two are relative to the stocks example and the flutter package respectively - # see https://github.com/dart-lang/sdk/issues/28463 - - "lib/i18n/stock_messages_*.dart" - - "lib/src/http/**" + # Ignore protoc generated files + - "dev/conductor/lib/proto/*" linter: rules: @@ -45,18 +16,22 @@ linter: # the Dart Lint rules page to make maintenance easier # https://github.com/dart-lang/linter/blob/master/example/all.yaml - always_declare_return_types + - always_put_control_body_on_new_line # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 - always_require_non_null_named_parameters - # - always_specify_types + - always_specify_types + # - always_use_package_imports # we do this commonly - annotate_overrides # - avoid_annotating_with_dynamic # conflicts with always_specify_types - - avoid_as - avoid_bool_literals_in_conditional_expressions # - avoid_catches_without_on_clauses # we do this commonly # - avoid_catching_errors # we do this commonly - avoid_classes_with_only_static_members # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_dynamic_calls - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + - avoid_escaping_inner_quotes - avoid_field_initializers_in_const_classes - avoid_function_literals_in_foreach_calls # - avoid_implementing_value_types # not yet tested @@ -64,7 +39,9 @@ linter: # - avoid_js_rounded_ints # only useful when targeting JS runtime - avoid_null_checks_in_equality_operators # - avoid_positional_boolean_parameters # not yet tested + # - avoid_print # not yet tested # - avoid_private_typedef_functions # we prefer having typedef (discussion in https://github.com/flutter/flutter/pull/16356) + # - avoid_redundant_argument_values # not yet tested - avoid_relative_lib_imports - avoid_renaming_method_parameters - avoid_return_types_on_setters @@ -73,42 +50,56 @@ linter: - avoid_returning_null_for_void # - avoid_returning_this # there are plenty of valid reasons to return this # - avoid_setters_without_getters # not yet tested - # - avoid_shadowing_type_parameters # not yet tested - # - avoid_single_cascade_in_expression_statements # not yet tested + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements - avoid_slow_async_io + - avoid_type_to_string - avoid_types_as_parameter_names # - avoid_types_on_closure_parameters # conflicts with always_specify_types + - avoid_unnecessary_containers - avoid_unused_constructor_parameters - avoid_void_async + # - avoid_web_libraries_in_flutter # not yet tested - await_only_futures + - camel_case_extensions - camel_case_types - cancel_subscriptions # - cascade_invocations # not yet tested + - cast_nullable_to_non_nullable # - close_sinks # not reliable enough - # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 + # - comment_references # blocked on https://github.com/dart-lang/linter/issues/1142 # - constant_identifier_names # needs an opt-out https://github.com/dart-lang/linter/issues/204 - control_flow_in_finally - # - curly_braces_in_flow_control_structures # not yet tested + # - curly_braces_in_flow_control_structures # not required by flutter style # - diagnostic_describe_all_properties # not yet tested - directives_ordering + # - do_not_use_environment # we do this commonly - empty_catches - empty_constructor_bodies - empty_statements - # - file_names # not yet tested + - exhaustive_cases + - file_names + - flutter_style_todos - hash_and_equals - implementation_imports # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 - iterable_contains_unrelated_type - # - join_return_with_assignment # not yet tested + # - join_return_with_assignment # not required by flutter style + - leading_newlines_in_multiline_strings - library_names - library_prefixes - # - lines_longer_than_80_chars # not yet tested + # - lines_longer_than_80_chars # not required by flutter style - list_remove_unrelated_type # - literal_only_boolean_expressions # too many false positives: https://github.com/dart-lang/sdk/issues/34181 + - missing_whitespace_between_adjacent_strings - no_adjacent_strings_in_list + # - no_default_cases # too many false positives - no_duplicate_case_values + - no_logic_in_create_state + # - no_runtimeType_toString # ok in tests; we enable this only in packages/ - non_constant_identifier_names - # - null_closures # not yet tested + - null_check_on_nullable_type_parameter + - null_closures # - omit_local_variable_types # opposite of always_specify_types # - one_member_abstracts # too many false positives # - only_throw_errors # https://github.com/flutter/flutter/issues/5792 @@ -119,14 +110,14 @@ linter: # - parameter_assignments # we do this commonly - prefer_adjacent_string_concatenation - prefer_asserts_in_initializer_lists - # - prefer_asserts_with_message # not yet tested + # - prefer_asserts_with_message # not required by flutter style - prefer_collection_literals - prefer_conditional_assignment - prefer_const_constructors - prefer_const_constructors_in_immutables - prefer_const_declarations - prefer_const_literals_to_create_immutables - # - prefer_constructors_over_static_methods # not yet tested + # - prefer_constructors_over_static_methods # far too many false positives - prefer_contains # - prefer_double_quotes # opposite of prefer_single_quotes - prefer_equal_for_default_values @@ -134,57 +125,72 @@ linter: - prefer_final_fields - prefer_final_in_for_each - prefer_final_locals - # - prefer_for_elements_to_map_fromIterable # not yet tested + - prefer_for_elements_to_map_fromIterable - prefer_foreach - # - prefer_function_declarations_over_variables # not yet tested - # - prefer_generic_function_type_aliases - # - prefer_if_elements_to_conditional_expressions # not yet tested + - prefer_function_declarations_over_variables + - prefer_generic_function_type_aliases + - prefer_if_elements_to_conditional_expressions - prefer_if_null_operators - prefer_initializing_formals - prefer_inlined_adds - # - prefer_int_literals # not yet tested - # - prefer_interpolation_to_compose_strings # not yet tested + # - prefer_int_literals # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#use-double-literals-for-double-constants + - prefer_interpolation_to_compose_strings - prefer_is_empty - prefer_is_not_empty + - prefer_is_not_operator - prefer_iterable_whereType # - prefer_mixin # https://github.com/dart-lang/language/issues/32 - # - prefer_null_aware_operators # disable until NNBD, see https://github.com/flutter/flutter/pull/32711#issuecomment-492930932 + - prefer_null_aware_operators + # - prefer_relative_imports # incompatible with sub-package imports - prefer_single_quotes - prefer_spread_collections - prefer_typing_uninitialized_variables - prefer_void_to_null - # - provide_deprecation_message # not yet tested + - provide_deprecation_message # - public_member_api_docs # enabled on a case-by-case basis; see e.g. packages/analysis_options.yaml - recursive_getters + - sized_box_for_whitespace - slash_for_doc_comments # - sort_child_properties_last # not yet tested - sort_constructors_first - - sort_pub_dependencies + # - sort_pub_dependencies # prevents separating pinned transitive dependencies - sort_unnamed_constructors_first - test_types_in_equals - throw_in_finally + - tighten_type_of_initializing_formals # - type_annotate_public_apis # subset of always_specify_types - type_init_formals # - unawaited_futures # too many false positives - # - unnecessary_await_in_return # not yet tested + - unnecessary_await_in_return - unnecessary_brace_in_string_interps - unnecessary_const + # - unnecessary_final # conflicts with prefer_final_locals - unnecessary_getters_setters # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 - unnecessary_new - unnecessary_null_aware_assignments + - unnecessary_null_checks - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations - unnecessary_overrides - unnecessary_parenthesis + # - unnecessary_raw_strings # not yet tested - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations - unnecessary_this - unrelated_type_equality_checks # - unsafe_html # not yet tested - use_full_hex_values_for_flutter_colors - # - use_function_type_syntax_for_parameters # not yet tested + - use_function_type_syntax_for_parameters + # - use_if_null_to_convert_nulls_to_bools # not yet tested + - use_is_even_rather_than_modulo + - use_key_in_widget_constructors + - use_late_for_private_fields_and_variables + - use_raw_strings - use_rethrow_when_possible # - use_setters_to_change_properties # not yet tested # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 # - use_to_and_as_if_applicable # has false positives, so we prefer to catch this by code-review - valid_regexps - # - void_checks # not yet tested \ No newline at end of file + - void_checks diff --git a/example/lib/main.dart b/example/lib/main.dart index e4bf540..cadcf4b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,33 +1,38 @@ -import 'package:flutter/material.dart'; import 'package:date_field/date_field.dart'; +import 'package:flutter/material.dart'; void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( - inputDecorationTheme: - const InputDecorationTheme(border: OutlineInputBorder()), + inputDecorationTheme: const InputDecorationTheme( + border: OutlineInputBorder(), + ), primarySwatch: Colors.blue, ), - home: MyHomePage(), + home: const MyHomePage(), ); } } class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key}) : super(key: key); + @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { - DateTime selectedDate; + DateTime? selectedDate; @override Widget build(BuildContext context) { @@ -36,7 +41,7 @@ class _MyHomePageState extends State { padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ + children: [ const FlutterLogo(size: 100), const SizedBox(height: 20), const Text('DateField package showcase'), @@ -59,7 +64,7 @@ class _MyHomePageState extends State { ), Form( child: Column( - children: [ + children: [ DateTimeFormField( decoration: const InputDecoration( hintStyle: TextStyle(color: Colors.black45), @@ -69,7 +74,7 @@ class _MyHomePageState extends State { labelText: 'My Super Date Time Field', ), autovalidateMode: AutovalidateMode.always, - validator: (e) => + validator: (DateTime? e) => (e?.day ?? 0) == 1 ? 'Please not the first day' : null, onDateSelected: (DateTime value) { print(value); @@ -86,8 +91,11 @@ class _MyHomePageState extends State { ), mode: DateTimeFieldPickerMode.time, autovalidateMode: AutovalidateMode.always, - validator: (e) => - (e?.day ?? 0) == 1 ? 'Please not the first day' : null, + validator: (DateTime? e) { + return (e?.day ?? 0) == 1 + ? 'Please not the first day' + : null; + }, onDateSelected: (DateTime value) { print(value); }, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index e263a92..2a79e78 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dev_dependencies: flutter_test: @@ -21,7 +21,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + cupertino_icons: ^1.0.3 flutter: diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart deleted file mode 100644 index 747db1d..0000000 --- a/example/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:example/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/lib/date_field.dart b/lib/date_field.dart index 7b832f0..867a09f 100644 --- a/lib/date_field.dart +++ b/lib/date_field.dart @@ -1,325 +1,2 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -final DateTime _kDefaultFirstSelectableDate = DateTime(1900); -final DateTime _kDefaultLastSelectableDate = DateTime(2100); - -const double _kCupertinoDatePickerHeight = 216; - -/// A [FormField] that contains a [DateTimeField]. -/// -/// This is a convenience widget that wraps a [DateTimeField] widget in a -/// [FormField]. -/// -/// A [Form] ancestor is not required. The [Form] simply makes it easier to -/// save, reset, or validate multiple fields at once. To use without a [Form], -/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to -/// save or reset the form field. -class DateTimeFormField extends FormField { - DateTimeFormField({ - Key? key, - FormFieldSetter? onSaved, - FormFieldValidator? validator, - DateTime? initialValue, - AutovalidateMode? autovalidateMode, - bool enabled = true, - TextStyle? dateTextStyle, - DateFormat? dateFormat, - DateTime? firstDate, - DateTime? lastDate, - ValueChanged? onDateSelected, - InputDecoration? decoration, - DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar, - DatePickerMode initialDatePickerMode = DatePickerMode.day, - DateTimeFieldPickerMode mode = DateTimeFieldPickerMode.dateAndTime, - }) : super( - key: key, - initialValue: initialValue, - onSaved: onSaved, - validator: validator, - autovalidateMode: autovalidateMode, - enabled: enabled, - builder: (FormFieldState field) { - // Theme defaults are applied inside the _InputDropdown widget - final InputDecoration _decorationWithThemeDefaults = - decoration ?? const InputDecoration(); - - final InputDecoration effectiveDecoration = - _decorationWithThemeDefaults.copyWith( - errorText: field.errorText); - - void onChangedHandler(DateTime value) { - if (onDateSelected != null) { - onDateSelected(value); - } - field.didChange(value); - } - - return DateTimeField( - firstDate: firstDate, - lastDate: lastDate, - decoration: effectiveDecoration, - initialDatePickerMode: initialDatePickerMode, - dateFormat: dateFormat, - onDateSelected: onChangedHandler, - selectedDate: field.value, - enabled: enabled, - mode: mode, - initialEntryMode: initialEntryMode, - dateTextStyle: dateTextStyle, - ); - }, - ); - - @override - _DateFormFieldState createState() => _DateFormFieldState(); -} - -class _DateFormFieldState extends FormFieldState {} - -/// [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 { - 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, - DateTime? firstDate, - DateTime? lastDate, - DateFormat? dateFormat, - }) : 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.dateTextStyle, - this.initialEntryMode = DatePickerEntryMode.calendar, - 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; - - /// 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 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; - - /// Shows a dialog asking the user to pick a date ! - Future _selectDate(BuildContext context) async { - final DateTime initialDateTime = selectedDate ?? DateTime.now(); - - if (Theme.of(context).platform == TargetPlatform.iOS) { - showModalBottomSheet( - context: context, - builder: (BuildContext builder) { - return Container( - height: _kCupertinoDatePickerHeight, - child: CupertinoDatePicker( - mode: _cupertinoModeFromPickerMode(mode), - onDateTimeChanged: onDateSelected!, - initialDateTime: initialDateTime, - minimumDate: firstDate, - maximumDate: lastDate, - ), - ); - }, - ); - } else { - DateTime _selectedDateTime = initialDateTime; - - if ([DateTimeFieldPickerMode.dateAndTime, DateTimeFieldPickerMode.date] - .contains(mode)) { - final DateTime? _selectedDate = await showDatePicker( - context: context, - initialDatePickerMode: initialDatePickerMode!, - initialDate: initialDateTime, - initialEntryMode: initialEntryMode, - firstDate: firstDate, - lastDate: lastDate, - ); - - if (_selectedDate != null) { - _selectedDateTime = _selectedDate; - } else { - return; - } - } - - if ([DateTimeFieldPickerMode.dateAndTime, DateTimeFieldPickerMode.time] - .contains(mode)) { - final TimeOfDay? _selectedTime = await showTimePicker( - initialTime: TimeOfDay.fromDateTime(initialDateTime), - context: context, - ); - - if (_selectedTime != null) { - _selectedDateTime = DateTime( - _selectedDateTime.year, - _selectedDateTime.month, - _selectedDateTime.day, - _selectedTime.hour, - _selectedTime.minute, - ); - } - } - - onDateSelected!(_selectedDateTime); - } - } - - @override - Widget build(BuildContext context) { - String? text; - - if (selectedDate != null) text = dateFormat.format(selectedDate!); - - TextStyle? textStyle; - - if (text == null) { - textStyle = decoration!.hintStyle ?? - Theme.of(context).inputDecorationTheme.hintStyle; - } else { - textStyle = dateTextStyle ?? dateTextStyle; - } - - final bool shouldDisplayLabelText = (text ?? decoration!.hintText) != null; - - InputDecoration? effectiveDecoration = decoration; - - if (!shouldDisplayLabelText) { - effectiveDecoration = effectiveDecoration!.copyWith(labelText: ''); - } - - return _InputDropdown( - text: text ?? - decoration!.hintText ?? - decoration!.labelText ?? - 'Select date', - textStyle: textStyle, - decoration: effectiveDecoration, - 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 StatelessWidget { - const _InputDropdown({ - Key? key, - required this.text, - this.decoration, - this.textStyle, - this.onPressed, - }) : 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; - - @override - Widget build(BuildContext context) { - final InputDecoration effectiveDecoration = decoration ?? - const InputDecoration( - suffixIcon: Icon(Icons.arrow_drop_down), - ).applyDefaults(Theme.of(context).inputDecorationTheme); - - return GestureDetector( - onTap: onPressed, - child: InputDecorator( - decoration: effectiveDecoration, - child: Text(text, style: textStyle), - ), - ); - } -} +export 'package:date_field/src/field.dart'; +export 'package:date_field/src/form_field.dart'; diff --git a/lib/src/field.dart b/lib/src/field.dart new file mode 100644 index 0000000..5c8abf9 --- /dev/null +++ b/lib/src/field.dart @@ -0,0 +1,262 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +final DateTime _kDefaultFirstSelectableDate = DateTime(1900); +final DateTime _kDefaultLastSelectableDate = DateTime(2100); + +const double _kCupertinoDatePickerHeight = 216; + +/// [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 { + 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, + DateTime? firstDate, + DateTime? lastDate, + DateFormat? dateFormat, + }) : 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.dateTextStyle, + this.initialEntryMode = DatePickerEntryMode.calendar, + 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; + + /// 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 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; + + /// Shows a dialog asking the user to pick a date ! + Future _selectDate(BuildContext context) async { + final DateTime initialDateTime = selectedDate ?? DateTime.now(); + + if (Theme.of(context).platform == TargetPlatform.iOS) { + showModalBottomSheet( + context: context, + builder: (BuildContext builder) { + return SizedBox( + height: _kCupertinoDatePickerHeight, + child: CupertinoDatePicker( + mode: _cupertinoModeFromPickerMode(mode), + onDateTimeChanged: onDateSelected!, + initialDateTime: initialDateTime, + minimumDate: firstDate, + maximumDate: lastDate, + ), + ); + }, + ); + } else { + DateTime _selectedDateTime = initialDateTime; + + const List modesWithDate = + [ + DateTimeFieldPickerMode.dateAndTime, + DateTimeFieldPickerMode.date + ]; + + if (modesWithDate.contains(mode)) { + final DateTime? _selectedDate = await showDatePicker( + context: context, + initialDatePickerMode: initialDatePickerMode!, + initialDate: initialDateTime, + initialEntryMode: initialEntryMode, + firstDate: firstDate, + lastDate: lastDate, + ); + + if (_selectedDate != null) { + _selectedDateTime = _selectedDate; + } else { + return; + } + } + + final List modesWithTime = + [ + DateTimeFieldPickerMode.dateAndTime, + DateTimeFieldPickerMode.time + ]; + + if (modesWithTime.contains(mode)) { + final TimeOfDay? _selectedTime = await showTimePicker( + initialTime: TimeOfDay.fromDateTime(initialDateTime), + context: context, + ); + + if (_selectedTime != null) { + _selectedDateTime = DateTime( + _selectedDateTime.year, + _selectedDateTime.month, + _selectedDateTime.day, + _selectedTime.hour, + _selectedTime.minute, + ); + } + } + + onDateSelected!(_selectedDateTime); + } + } + + @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 StatelessWidget { + 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 + Widget build(BuildContext context) { + final InputDecoration effectiveDecoration = decoration ?? + const InputDecoration( + suffixIcon: Icon(Icons.arrow_drop_down), + ); + + return GestureDetector( + onTap: onPressed, + child: InputDecorator( + decoration: effectiveDecoration.applyDefaults( + Theme.of(context).inputDecorationTheme, + ), + isEmpty: isEmpty, + child: text == null ? null : Text(text!, style: textStyle), + ), + ); + } +} diff --git a/lib/src/form_field.dart b/lib/src/form_field.dart new file mode 100644 index 0000000..45afc4c --- /dev/null +++ b/lib/src/form_field.dart @@ -0,0 +1,75 @@ +import 'package:date_field/src/field.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A [FormField] that contains a [DateTimeField]. +/// +/// This is a convenience widget that wraps a [DateTimeField] widget in a +/// [FormField]. +/// +/// A [Form] ancestor is not required. The [Form] simply makes it easier to +/// save, reset, or validate multiple fields at once. To use without a [Form], +/// pass a [GlobalKey] to the constructor and use [GlobalKey.currentState] to +/// save or reset the form field. +class DateTimeFormField extends FormField { + DateTimeFormField({ + Key? key, + FormFieldSetter? onSaved, + FormFieldValidator? validator, + DateTime? initialValue, + AutovalidateMode? autovalidateMode, + bool enabled = true, + TextStyle? dateTextStyle, + DateFormat? dateFormat, + DateTime? firstDate, + DateTime? lastDate, + ValueChanged? onDateSelected, + InputDecoration? decoration, + DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar, + DatePickerMode initialDatePickerMode = DatePickerMode.day, + DateTimeFieldPickerMode mode = DateTimeFieldPickerMode.dateAndTime, + }) : super( + key: key, + initialValue: initialValue, + onSaved: onSaved, + validator: validator, + autovalidateMode: autovalidateMode, + enabled: enabled, + builder: (FormFieldState field) { + // Theme defaults are applied inside the _InputDropdown widget + final InputDecoration _decorationWithThemeDefaults = + decoration ?? const InputDecoration(); + + final InputDecoration effectiveDecoration = + _decorationWithThemeDefaults.copyWith( + errorText: field.errorText); + + void onChangedHandler(DateTime value) { + if (onDateSelected != null) { + onDateSelected(value); + } + field.didChange(value); + } + + return DateTimeField( + firstDate: firstDate, + lastDate: lastDate, + decoration: effectiveDecoration, + initialDatePickerMode: initialDatePickerMode, + dateFormat: dateFormat, + onDateSelected: onChangedHandler, + selectedDate: field.value, + enabled: enabled, + mode: mode, + initialEntryMode: initialEntryMode, + dateTextStyle: dateTextStyle, + ); + }, + ); + + @override + _DateFormFieldState createState() => _DateFormFieldState(); +} + +class _DateFormFieldState extends FormFieldState {} diff --git a/pubspec.yaml b/pubspec.yaml index 3dec2f4..ce71900 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: date_field description: A widget in the form of a field that lets people choose a date, a time or both. -version: 2.0.1 +version: 2.1.0 homepage: 'https://github.com/GaspardMerten/date_field' environment: