From 9064d95483d66f0cb004db495c4869aa1765738c Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 14 Jun 2023 12:29:22 +0700 Subject: [PATCH] TF-1884 Support RTL mode for TextFormField (cherry picked from commit 7c12f65a037bb5a1b6cef91938af1ec1dac20a9b) --- core/lib/core.dart | 1 + .../dialog/edit_text_dialog_builder.dart | 6 +- .../edit_text_modal_sheet_builder.dart | 7 +- .../views/text/input_decoration_builder.dart | 4 +- .../views/text/text_form_field_builder.dart | 120 +++++++++++ .../widgets/insert_image_dialog_builder.dart | 186 ------------------ .../widgets/login_text_input_builder.dart | 14 +- 7 files changed, 138 insertions(+), 200 deletions(-) create mode 100644 core/lib/presentation/views/text/text_form_field_builder.dart delete mode 100644 lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart diff --git a/core/lib/core.dart b/core/lib/core.dart index 230e637b9..0a3b3391a 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -46,6 +46,7 @@ export 'utils/platform_info.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; export 'presentation/views/text/text_field_builder.dart'; +export 'presentation/views/text/text_form_field_builder.dart'; export 'presentation/views/text/input_decoration_builder.dart'; export 'presentation/views/text/rich_text_builder.dart'; export 'presentation/views/responsive/responsive_widget.dart'; diff --git a/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart b/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart index a3c9860aa..00536b195 100644 --- a/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart @@ -102,10 +102,10 @@ class EditTextDialogBuilder { textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 20), - child: TextFormField( + child: TextFormFieldBuilder( keyboardType: TextInputType.visiblePassword, - onChanged: (value) => _onTextChanged(value, setState), - autofocus: true, + onTextChange: (value) => _onTextChanged(value, setState), + autoFocus: true, controller: _textController, decoration: InputDecoration( errorText: _error, diff --git a/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart b/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart index b7a8c62be..ee94e137a 100644 --- a/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart +++ b/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -126,10 +127,10 @@ class EditTextModalSheetBuilder { textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 20), - child: TextFormField( + child: TextFormFieldBuilder( keyboardType: TextInputType.visiblePassword, - onChanged: (value) => _onTextChanged(value, setState), - autofocus: true, + onTextChange: (value) => _onTextChanged(value, setState), + autoFocus: true, controller: _textController, decoration: InputDecoration( errorText: _error, diff --git a/core/lib/presentation/views/text/input_decoration_builder.dart b/core/lib/presentation/views/text/input_decoration_builder.dart index 9225bbbd9..5b5a7dcfc 100644 --- a/core/lib/presentation/views/text/input_decoration_builder.dart +++ b/core/lib/presentation/views/text/input_decoration_builder.dart @@ -7,7 +7,7 @@ abstract class InputDecorationBuilder { TextStyle? labelStyle; String? hintText; TextStyle? hintStyle; - EdgeInsets? contentPadding; + EdgeInsetsGeometry? contentPadding; OutlineInputBorder? enabledBorder; OutlineInputBorder? errorBorder; OutlineInputBorder? focusBorder; @@ -43,7 +43,7 @@ abstract class InputDecorationBuilder { hintStyle = newHintStyle; } - void setContentPadding(EdgeInsets? newContentPadding) { + void setContentPadding(EdgeInsetsGeometry? newContentPadding) { contentPadding = newContentPadding; } diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart new file mode 100644 index 000000000..cc21b8070 --- /dev/null +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -0,0 +1,120 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; + +class TextFormFieldBuilder extends StatefulWidget { + + final ValueChanged? onTextChange; + final ValueChanged? onTextSubmitted; + final VoidCallback? onTap; + final TextStyle? textStyle; + final TextInputAction? textInputAction; + final InputDecoration? decoration; + final bool obscureText; + final int? maxLines; + final int? minLines; + final TextEditingController? controller; + final TextInputType? keyboardType; + final Color cursorColor; + final bool autoFocus; + final FocusNode? focusNode; + final String? fromValue; + final Brightness? keyboardAppearance; + final bool autocorrect; + final TextDirection textDirection; + final bool readOnly; + final MouseCursor? mouseCursor; + final List? autofillHints; + + const TextFormFieldBuilder({ + super.key, + this.cursorColor = AppColor.primaryColor, + this.autocorrect = false, + this.obscureText = false, + this.autoFocus = false, + this.readOnly = false, + this.textStyle = const TextStyle(color: AppColor.textFieldTextColor), + this.textDirection = TextDirection.ltr, + this.textInputAction, + this.decoration, + this.maxLines = 1, + this.minLines, + this.controller, + this.keyboardType, + this.focusNode, + this.fromValue, + this.keyboardAppearance, + this.mouseCursor, + this.autofillHints, + this.onTap, + this.onTextChange, + this.onTextSubmitted, + }); + + @override + State createState() => _TextFieldFormBuilderState(); +} + +class _TextFieldFormBuilderState extends State { + + late TextEditingController _controller; + late TextDirection _textDirection; + + @override + void initState() { + if (widget.fromValue != null) { + _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); + } else if (widget.controller != null) { + _controller = widget.controller!; + } else { + _controller = TextEditingController(); + } + _textDirection = widget.textDirection; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + autofillHints: widget.autofillHints, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onFieldSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + ); + } + + @override + void dispose() { + if (widget.fromValue == null && widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart b/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart deleted file mode 100644 index 15f7d4ef1..000000000 --- a/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart +++ /dev/null @@ -1,186 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:model/upload/file_info.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; - -typedef InsertImageActionCallback = Function(InlineImage image); - -class InsertImageDialogBuilder { - - final _inputFileController = TextEditingController(); - final _inputUrlController = TextEditingController(); - - final InsertImageActionCallback? insertActionCallback; - final BuildContext _context; - - FileInfo? fileSelected; - String? validateFailed; - - InsertImageDialogBuilder(this._context, { - this.insertActionCallback - }); - - Future show() async { - await showDialog( - context: _context, - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - builder: (BuildContext context) { - return PointerInterceptor( - child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - return AlertDialog( - title: Text(AppLocalizations.of(context).insertImage, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black)), - titleTextStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black), - titlePadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), - scrollable: true, - elevation: 10, - content: Container( - color: Colors.white, - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(AppLocalizations.of(context).selectFromFile, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.w500)), - const SizedBox(height: 10), - TextFormField( - controller: _inputFileController, - readOnly: true, - decoration: InputDecoration( - prefixIcon: buildButtonWrapText( - AppLocalizations.of(context).chooseImage, - radius: 5, - height: 30, - padding: const EdgeInsets.only(right: 8), - textStyle: const TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.normal), - bgColor: AppColor.colorShadowComposer, - onTap: () => _selectFromFile(setState)), - suffixIcon: fileSelected != null - ? IconButton( - splashRadius: 10, - icon: const Icon(Icons.close), - onPressed: () { - setState(() { - fileSelected = null; - _inputFileController.text = ''; - }); - }) - : const SizedBox.shrink(), - errorText: validateFailed, - errorMaxLines: 2, - border: InputBorder.none, - )), - const SizedBox(height: 20), - Text(AppLocalizations.of(context).urlLink, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.w500)), - const SizedBox(height: 10), - TextFieldBuilder( - controller: _inputUrlController, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: AppLocalizations.of(context).urlLink, - errorText: validateFailed, - errorMaxLines: 2, - ), - ) - ]), - ), - actionsPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - actions: [ - buildButtonWrapText( - AppLocalizations.of(context).cancel, - radius: 5, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.normal), - bgColor: AppColor.colorShadowComposer, - onTap: () => popBack()), - buildButtonWrapText( - AppLocalizations.of(context).insert, - radius: 5, - textStyle: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500), - onTap: () => _insertImageAction(context, setState)) - ], - ); - }), - ); - }); - } - - void _selectFromFile(StateSetter setState) async { - final filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.image, - withReadStream: true); - - final platformFile = filePickerResult?.files.single; - if (platformFile != null) { - fileSelected = FileInfo( - platformFile.name, - BuildUtils.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - readStream: platformFile.readStream); - - setState(() { - _inputFileController.text = fileSelected!.fileName; - }); - } - } - - void _insertImageAction(BuildContext context, StateSetter setState) { - final inputFile = _inputFileController.text; - final inputUrl = _inputUrlController.text; - - if (inputFile.isEmpty && inputUrl.isEmpty) { - setState(() { - validateFailed = AppLocalizations.of(context).insertImageErrorFileEmpty; - }); - } else if (inputFile.isNotEmpty && inputUrl.isNotEmpty) { - setState(() { - validateFailed = AppLocalizations.of(context).insertImageErrorDuplicate; - }); - } else if (inputFile.isNotEmpty && fileSelected != null) { - if (insertActionCallback != null) { - insertActionCallback!.call(InlineImage(ImageSource.local, fileInfo: fileSelected)); - } - popBack(); - } else { - if (insertActionCallback != null) { - insertActionCallback!.call(InlineImage(ImageSource.network, link: inputUrl)); - } - popBack(); - } - } -} \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/login_text_input_builder.dart b/lib/features/login/presentation/widgets/login_text_input_builder.dart index 25785f66e..b04d7e737 100644 --- a/lib/features/login/presentation/widgets/login_text_input_builder.dart +++ b/lib/features/login/presentation/widgets/login_text_input_builder.dart @@ -1,6 +1,8 @@ import 'dart:async'; -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -122,21 +124,21 @@ class LoginTextInputBuilder { child: Stack( alignment: AlignmentDirectional.centerEnd, children: [ - TextFormField( - onFieldSubmitted: _onSubmitted, - onChanged: (value) => _onTextChanged(value, setState), + TextFormFieldBuilder( + onTextSubmitted: _onSubmitted, + onTextChange: (value) => _onTextChanged(value, setState), obscureText: _obscureText ?? false, textInputAction: _textInputAction, autofillHints: autofillHints, autocorrect: autocorrect, controller: _textEditingController, - cursorColor: AppColor.primaryColor, - style: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal), + textStyle: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal), focusNode: _focusNode, decoration: (LoginInputDecorationBuilder() ..setHintText(_hintText) ..setPrefixText(_prefixText) ..setErrorText(_errorText) + ..setContentPadding(const EdgeInsetsDirectional.only(start: 25, top: 15, bottom: 15, end: 40)) ..setHintStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal)) ..setPrefixStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal)) ..setErrorTextStyle(const TextStyle(color: AppColor.loginTextFieldErrorBorder, fontSize: 13, fontWeight: FontWeight.normal))