runable
This commit is contained in:
271
lib/shared/widgets/vietnamese_phone_field.dart
Normal file
271
lib/shared/widgets/vietnamese_phone_field.dart
Normal file
@@ -0,0 +1,271 @@
|
||||
/// Vietnamese Phone Number Input Field
|
||||
///
|
||||
/// Specialized input field for Vietnamese phone numbers with
|
||||
/// auto-formatting and validation.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../core/utils/validators.dart';
|
||||
import '../../core/utils/formatters.dart';
|
||||
import '../../core/constants/ui_constants.dart';
|
||||
|
||||
/// Phone number input field with Vietnamese formatting
|
||||
class VietnamesePhoneField extends StatefulWidget {
|
||||
final TextEditingController? controller;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final String? initialValue;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final ValueChanged<String>? onSubmitted;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final bool enabled;
|
||||
final bool autoFocus;
|
||||
final TextInputAction? textInputAction;
|
||||
final FocusNode? focusNode;
|
||||
final bool required;
|
||||
final Widget? prefixIcon;
|
||||
final Widget? suffixIcon;
|
||||
|
||||
const VietnamesePhoneField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.initialValue,
|
||||
this.onChanged,
|
||||
this.onSubmitted,
|
||||
this.validator,
|
||||
this.enabled = true,
|
||||
this.autoFocus = false,
|
||||
this.textInputAction,
|
||||
this.focusNode,
|
||||
this.required = true,
|
||||
this.prefixIcon,
|
||||
this.suffixIcon,
|
||||
});
|
||||
|
||||
@override
|
||||
State<VietnamesePhoneField> createState() => _VietnamesePhoneFieldState();
|
||||
}
|
||||
|
||||
class _VietnamesePhoneFieldState extends State<VietnamesePhoneField> {
|
||||
late TextEditingController _controller;
|
||||
bool _isControllerInternal = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController(text: widget.initialValue);
|
||||
_isControllerInternal = true;
|
||||
} else {
|
||||
_controller = widget.controller!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_isControllerInternal) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
focusNode: widget.focusNode,
|
||||
enabled: widget.enabled,
|
||||
autofocus: widget.autoFocus,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: widget.textInputAction ?? TextInputAction.next,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(11),
|
||||
_PhoneNumberFormatter(),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText ?? 'Số điện thoại',
|
||||
hintText: widget.hintText ?? '0xxx xxx xxx',
|
||||
prefixIcon: widget.prefixIcon ??
|
||||
const Icon(Icons.phone),
|
||||
suffixIcon: widget.suffixIcon,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
),
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
),
|
||||
validator: widget.validator ??
|
||||
(widget.required ? Validators.phone : Validators.phoneOptional),
|
||||
onChanged: widget.onChanged,
|
||||
onFieldSubmitted: widget.onSubmitted,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Phone number text input formatter
|
||||
class _PhoneNumberFormatter extends TextInputFormatter {
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
final text = newValue.text;
|
||||
|
||||
if (text.isEmpty) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
// Format as: 0xxx xxx xxx
|
||||
String formatted = text;
|
||||
if (text.length > 4 && text.length <= 7) {
|
||||
formatted = '${text.substring(0, 4)} ${text.substring(4)}';
|
||||
} else if (text.length > 7) {
|
||||
formatted =
|
||||
'${text.substring(0, 4)} ${text.substring(4, 7)} ${text.substring(7)}';
|
||||
}
|
||||
|
||||
return TextEditingValue(
|
||||
text: formatted,
|
||||
selection: TextSelection.collapsed(offset: formatted.length),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read-only phone display field
|
||||
class PhoneDisplayField extends StatelessWidget {
|
||||
final String phoneNumber;
|
||||
final String? labelText;
|
||||
final Widget? prefixIcon;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const PhoneDisplayField({
|
||||
super.key,
|
||||
required this.phoneNumber,
|
||||
this.labelText,
|
||||
this.prefixIcon,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
initialValue: PhoneFormatter.format(phoneNumber),
|
||||
readOnly: true,
|
||||
enabled: onTap != null,
|
||||
onTap: onTap,
|
||||
decoration: InputDecoration(
|
||||
labelText: labelText ?? 'Số điện thoại',
|
||||
prefixIcon: prefixIcon ?? const Icon(Icons.phone),
|
||||
suffixIcon: onTap != null ? const Icon(Icons.edit) : null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
),
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Phone field with country code selector
|
||||
class InternationalPhoneField extends StatefulWidget {
|
||||
final TextEditingController? controller;
|
||||
final String? labelText;
|
||||
final String? hintText;
|
||||
final ValueChanged<String>? onChanged;
|
||||
final FormFieldValidator<String>? validator;
|
||||
final bool enabled;
|
||||
final String defaultCountryCode;
|
||||
|
||||
const InternationalPhoneField({
|
||||
super.key,
|
||||
this.controller,
|
||||
this.labelText,
|
||||
this.hintText,
|
||||
this.onChanged,
|
||||
this.validator,
|
||||
this.enabled = true,
|
||||
this.defaultCountryCode = '+84',
|
||||
});
|
||||
|
||||
@override
|
||||
State<InternationalPhoneField> createState() =>
|
||||
_InternationalPhoneFieldState();
|
||||
}
|
||||
|
||||
class _InternationalPhoneFieldState extends State<InternationalPhoneField> {
|
||||
late TextEditingController _controller;
|
||||
late String _selectedCountryCode;
|
||||
bool _isControllerInternal = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_selectedCountryCode = widget.defaultCountryCode;
|
||||
|
||||
if (widget.controller == null) {
|
||||
_controller = TextEditingController();
|
||||
_isControllerInternal = true;
|
||||
} else {
|
||||
_controller = widget.controller!;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_isControllerInternal) {
|
||||
_controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFormField(
|
||||
controller: _controller,
|
||||
enabled: widget.enabled,
|
||||
keyboardType: TextInputType.phone,
|
||||
textInputAction: TextInputAction.next,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(10),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: widget.labelText ?? 'Số điện thoại',
|
||||
hintText: widget.hintText ?? 'xxx xxx xxx',
|
||||
prefixIcon: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: DropdownButton<String>(
|
||||
value: _selectedCountryCode,
|
||||
underline: const SizedBox(),
|
||||
items: const [
|
||||
DropdownMenuItem(value: '+84', child: Text('+84')),
|
||||
DropdownMenuItem(value: '+1', child: Text('+1')),
|
||||
DropdownMenuItem(value: '+86', child: Text('+86')),
|
||||
],
|
||||
onChanged: widget.enabled
|
||||
? (value) {
|
||||
if (value != null) {
|
||||
setState(() {
|
||||
_selectedCountryCode = value;
|
||||
});
|
||||
widget.onChanged?.call('$value${_controller.text}');
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||
),
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
),
|
||||
validator: widget.validator,
|
||||
onChanged: (value) {
|
||||
widget.onChanged?.call('$_selectedCountryCode$value');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user