TF-1884 Support RTL mode for TextFormField
(cherry picked from commit 7c12f65a037bb5a1b6cef91938af1ec1dac20a9b)
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>? onTextChange;
|
||||
final ValueChanged<String>? 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<String>? 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<TextFormFieldBuilder> createState() => _TextFieldFormBuilderState();
|
||||
}
|
||||
|
||||
class _TextFieldFormBuilderState extends State<TextFormFieldBuilder> {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user