273 lines
7.7 KiB
Dart
273 lines
7.7 KiB
Dart
/// 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 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:worker/core/constants/ui_constants.dart';
|
|
import 'package:worker/core/utils/formatters.dart';
|
|
import 'package:worker/core/utils/validators.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 FaIcon(FontAwesomeIcons.phone, size: 20),
|
|
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 FaIcon(FontAwesomeIcons.phone, size: 20),
|
|
suffixIcon: onTap != null ? const FaIcon(FontAwesomeIcons.penToSquare, size: 18) : 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');
|
|
},
|
|
);
|
|
}
|
|
}
|