diff --git a/lib/src/Fields/fields.astromic.dart b/lib/src/Fields/fields.astromic.dart index 197bbee..c4eb6c3 100644 --- a/lib/src/Fields/fields.astromic.dart +++ b/lib/src/Fields/fields.astromic.dart @@ -1,6 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; // +import 'src/action_field.dart'; import 'src/text_field.dart'; import 'src/configuration.dart'; import 'src/style.dart'; @@ -27,7 +28,6 @@ class AstromicFields { // required AstromicFieldStyle Function(bool isEnabled, bool isFocused) style, // - //s1 -- Content String? hint, // Widget? Function(bool isEnabled, bool isFocused, VoidCallback stateSetter)? prefixWidget, @@ -57,4 +57,49 @@ class AstromicFields { ), ], ); + + //S1 -- Action Field + static Widget action( + {Key? stateKey, + (T item, String label)? initialValue, + required TextEditingController controller, + // + Future<(T item, String label)?> Function((T item, String label)? currentValue)? onTap, + Future<(T item, String label)?> Function((T item, String label)? currentValue)? onHold, + required String Function(String? oldValue, String newValue) textFieldMapper, + // + required AstromicFieldConfiguration configuration, + // + AutovalidateMode? validatingMode, + String? Function(bool isEnabled, String? text)? validator, + List? inputFormatters, + // + required AstromicFieldStyle Function(bool isEnabled) style, + // + String? hint, + // + Widget? Function(bool isEnabled, VoidCallback stateSetter)? prefixWidget, + Widget? Function(bool isEnabled, VoidCallback stateSetter)? suffixWidget, + Widget? Function(bool isEnabled)? messageBuilder}) => + Column( + children: [ + AstromicActionField( + stateKey: stateKey, + textController: controller, + initialValue: initialValue, + onTap: onTap, + onHold: onHold, + textFieldMapper: textFieldMapper, + configuration: configuration, + validatingMode: validatingMode, + validator: validator, + inputFormatters: inputFormatters, + style: style, + hint: hint, + prefixWidget: prefixWidget, + suffixWidget: suffixWidget, + messageBuilder: messageBuilder, + ), + ], + ); } diff --git a/lib/src/Fields/src/action_field.dart b/lib/src/Fields/src/action_field.dart new file mode 100644 index 0000000..921a57b --- /dev/null +++ b/lib/src/Fields/src/action_field.dart @@ -0,0 +1,424 @@ +// ignore_for_file: depend_on_referenced_packages +//SECTION - Imports +// +//s1 PACKAGES +//--------------- +//s2 CORE +import 'package:astromic_mobile_elements/Infrastructure/insets_extension.dart'; +import 'package:astromic_mobile_elements/Infrastructure/misc_extensions.dart'; +import 'package:astromic_mobile_elements/astromic_mobile_elements.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart' as intl; +import 'dart:ui' as ui; + +//s2 3RD-PARTY +// +//s1 DEPENDENCIES +//--------------- +//s2 SERVICES +//s2 MODELS + +//s2 MISC + +//!SECTION - Imports +// +//SECTION - Exports +//!SECTION - Exports +// +class AstromicActionField extends StatefulWidget { + //SECTION - Widget Arguments + //s1 -- Functionality + final Key? stateKey; + final (T item, String label)? initialValue; + final TextEditingController textController; + final Future<(T item, String label)?> Function((T item, String label)? currentValue)? onTap; + final Future<(T item, String label)?> Function((T item, String label)? currentValue)? onHold; + final String Function(String? oldValue, String newValue) textFieldMapper; + // + //s1 -- Configurations + final AstromicFieldConfiguration configuration; + // + final AutovalidateMode? validatingMode; + final String? Function(bool isEnabled, String? text)? validator; + final List? inputFormatters; + final bool? obscureText; + // + //s1 -- Style + final AstromicFieldStyle Function(bool isEnabled) style; + // + //s1 -- Content + final String? hint; + // + final Widget? Function(bool isEnabled, VoidCallback stateSetter)? prefixWidget; + final Widget? Function(bool isEnabled, VoidCallback stateSetter)? suffixWidget; + final Widget? Function(bool isEnabled)? messageBuilder; + //!SECTION + // + const AstromicActionField({ + super.key, + // + this.initialValue, + this.stateKey, + required this.textController, + required this.textFieldMapper, + this.onTap, + this.onHold, + // + required this.configuration, + // + this.validatingMode, + this.validator, + this.inputFormatters, + this.obscureText, + // + required this.style, + // + this.hint, + this.prefixWidget, + this.suffixWidget, + this.messageBuilder, + }); + + @override + State> createState() => _AstromicActionFieldState(); +} + +class _AstromicActionFieldState extends State> { + // + //SECTION - State Variables + //s1 --Controllers + late TextEditingController _textController; + //s1 --Controllers + // + //s1 --State + (T item, String label)? _currentValue; + // + //s1 --State + // + //s1 --Constants + //s1 --Constants + //!SECTION + + @override + void initState() { + super.initState(); + // + //SECTION - State Variables initializations & Listeners + //s1 --Controllers & Listeners + _currentValue = widget.initialValue; + _textController = widget.textController; + _textController.text = widget.initialValue?.$2 ?? ''; + //s1 --Controllers & Listeners + // + //s1 --State + //s1 --State + // + //s1 --Late & Async Initializers + //s1 --Late & Async Initializers + //!SECTION + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // + //SECTION - State Variables initializations & Listeners + //s1 --State + //s1 --State + //!SECTION + } + + //SECTION - Dumb Widgets + //!SECTION + + //SECTION - Stateless functions + getTextHeight(String text, TextStyle style, ui.TextDirection direction) { + final span = TextSpan(text: text, style: style); + final tp = TextPainter(text: span, textDirection: direction); + tp.layout(maxWidth: double.infinity); + return tp.height; + } + + //- + //---------------------------------------------------------------- + //- + + ui.TextDirection finalTextDirection() { + ui.TextDirection fromLocale = intl.Bidi.isRtlLanguage(Localizations.localeOf(context).languageCode) ? ui.TextDirection.rtl : ui.TextDirection.ltr; + // + if (widget.configuration.textDirection != null) { + // Get form Style + return widget.configuration.textDirection!; + } else { + // Detect form Text + return _textController.text != '' + ? widget.configuration.withAutomaticDirectionalitySwitching + ? intl.Bidi.startsWithLtr(_textController.text) + ? ui.TextDirection.ltr + : ui.TextDirection.rtl + : fromLocale + : fromLocale; + } + } + + //- + //---------------------------------------------------------------- + //- + EdgeInsetsGeometry finalVerticalPadding( + AstromicFieldStyle style, { + bool forceRespectBorder = false, + }) { + double desiredFixedHeight = style.fixedHeight ?? 0; + // + double providedPadding = style.contentPadding.vertical; + // + double borderOffset = widget.configuration.respectBorderWidthPadding || forceRespectBorder ? style.border?.borderSide.strokeInset ?? 0.0 : 0.0; + // + double fontOffset = getTextHeight( + widget.textController.text.isNotEmpty + ? widget.textController.text + : (widget.hint?.isNotEmpty ?? false) + ? widget.hint! + : "", + widget.textController.text.isNotEmpty && widget.style(widget.configuration.isEnabled).textStyle != null + ? widget.style(widget.configuration.isEnabled).textStyle! + : widget.style(widget.configuration.isEnabled).hintStyle != null + ? widget.style(widget.configuration.isEnabled).hintStyle! + : const TextStyle(), + finalTextDirection(), + ); + // + double finalVertical = + //- + widget.configuration.isFixedHeight + ? + //- + widget.configuration.isTextArea + ? + //s1 Fixed Height with Text Area + borderOffset + providedPadding + : + //s1 Fixed Height and not Text Area + desiredFixedHeight - (fontOffset + borderOffset) + //s1 - Not Fixed Height + : providedPadding + (borderOffset / 2); + // + return EdgeInsets.symmetric( + vertical: finalVertical / 2, + ); + } + //!SECTION + + //SECTION - Action Callbacks + //!SECTION + + @override + Widget build(BuildContext context) { + //SECTION - Build Setup + //s1 --Values + //double w = MediaQuery.of(context).size.width; + //double h = MediaQuery.of(context).size.height; + //s1 --Values + // + //s1 --Contexted Widgets + InputDecoration inputDecoration = InputDecoration( + //s1 -- Functionality + enabled: widget.configuration.isEnabled, + //s1 -- Configurations + isDense: true, + hintTextDirection: widget.configuration.hintDirection ?? finalTextDirection(), + //s1 -- Style + filled: widget.style(widget.configuration.isEnabled).isFilled, + fillColor: widget.style(widget.configuration.isEnabled).fillColor, + // + hintStyle: widget.style(widget.configuration.isEnabled).hintStyle, + // + enabledBorder: widget.style(widget.configuration.isEnabled).border, + focusedBorder: widget.style(widget.configuration.isEnabled).border, + disabledBorder: widget.style(widget.configuration.isEnabled).border, + errorBorder: widget.style(widget.configuration.isEnabled).border, + focusedErrorBorder: widget.style(widget.configuration.isEnabled).border, + // + errorStyle: const TextStyle(height: 0), + contentPadding: finalVerticalPadding(widget.style(widget.configuration.isEnabled)), + //s1 -- Content + hintText: widget.hint, + // + prefixIcon: widget.prefixWidget != null && + widget.prefixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }) != + null + ? Container( + margin: EdgeInsetsDirectional.fromSTEB( + widget.style(widget.configuration.isEnabled).prefixSpacing, 0, widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).start, 0), + child: widget.prefixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }), + ) + : SizedBox(width: widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).start), + // + prefixIconConstraints: widget.prefixWidget != null && + widget.prefixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }) != + null + ? widget.style(widget.configuration.isEnabled).prefixSize != 0 + ? BoxConstraints.expand( + width: widget.style(widget.configuration.isEnabled).prefixSize + + widget.style(widget.configuration.isEnabled).prefixSpacing + + widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).start, + height: widget.style(widget.configuration.isEnabled).prefixSize, + ) + : const BoxConstraints.tightForFinite() + : BoxConstraints.tightForFinite(width: widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).start), + // + suffixIcon: widget.suffixWidget != null && + widget.suffixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }) != + null + ? Container( + margin: EdgeInsetsDirectional.fromSTEB( + widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).end, 0, widget.style(widget.configuration.isEnabled).suffixSpacing, 0), + child: widget.suffixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }), + ) + : SizedBox(width: widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).end), + suffixIconConstraints: widget.suffixWidget != null && + widget.suffixWidget != null && + widget.suffixWidget!(widget.configuration.isEnabled, () { + setState(() {}); + }) != + null + ? widget.style(widget.configuration.isEnabled).suffixSize != 0 + ? BoxConstraints.expand( + width: widget.style(widget.configuration.isEnabled).suffixSize + + widget.style(widget.configuration.isEnabled).suffixSpacing + + widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).end, + height: widget.style(widget.configuration.isEnabled).suffixSize, + ) + : const BoxConstraints.tightForFinite() + : BoxConstraints.tightForFinite(width: widget.style(widget.configuration.isEnabled).contentPadding.resolveToDirectional(finalTextDirection()).end), + ); + //s1 --Contexted Widgets + //!SECTION + // + //SECTION - Build Return + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Directionality( + textDirection: finalTextDirection(), + child: Container( + height: widget.configuration.isFixedHeight + ? !widget.configuration.isTextArea + ? null + : widget.style(widget.configuration.isEnabled).fixedHeight + : null, + alignment: widget.style(widget.configuration.isEnabled).textAlignVertical.toAlignment(), + child: Stack( + children: [ + TextFormField( + //s1 -- Functionality + key: widget.stateKey, + controller: _textController, + textInputAction: widget.configuration.inputAction, + // + //s1 -- Configurations + autovalidateMode: widget.validatingMode, + autofocus: widget.configuration.withAutofocus, + keyboardType: widget.configuration.inputType, + textDirection: finalTextDirection(), + obscureText: widget.configuration.withObscurity ? (widget.obscureText ?? false) : false, + inputFormatters: widget.inputFormatters, + // + minLines: widget.configuration.isFixedHeight && widget.configuration.isTextArea ? null : widget.style(widget.configuration.isEnabled).minLines, + maxLines: widget.configuration.isFixedHeight + ? widget.configuration.isTextArea + ? null + : widget.style(widget.configuration.isEnabled).maxLines + : widget.style(widget.configuration.isEnabled).maxLines, + + // + maxLength: widget.style(widget.configuration.isEnabled).maxLength, + maxLengthEnforcement: widget.configuration.maxLengthEnforcement, + // + expands: widget.configuration.isFixedHeight && widget.configuration.isTextArea ? true : false, + // - Validation + validator: widget.validator != null ? (s) => widget.validator!(widget.configuration.isEnabled, s) : null, + // - Validation + //s1 -- Style + style: widget.style(widget.configuration.isEnabled).textStyle, + cursorColor: widget.style(widget.configuration.isEnabled).cursorColor, + textAlign: widget.style(widget.configuration.isEnabled).textAlign, + textAlignVertical: widget.style(widget.configuration.isEnabled).textAlignVertical, + // + //s1 -- Input Decoration + decoration: inputDecoration, + ), + SizedBox( + width: double.infinity, + height: finalVerticalPadding(widget.style(widget.configuration.isEnabled), forceRespectBorder: true).vertical * 2, + child: Material( + type: MaterialType.transparency, + child: InkWell( + onTap: () async { + if (widget.onTap != null) { + (T item, String label)? c = _currentValue; + (T item, String label)? newValue = await widget.onTap!(_currentValue); + if (newValue != null) { + setState(() { + _currentValue = newValue; + _textController.text = widget.textFieldMapper(c?.$2, newValue.$2); + }); + } + } + }, + splashFactory: NoSplash.splashFactory, + onLongPress: () async { + if (widget.onHold != null) { + (T item, String label)? c = _currentValue; + (T item, String label)? newValue = await widget.onHold!(_currentValue); + if (newValue != null) { + setState(() { + _currentValue = newValue; + _textController.text = widget.textFieldMapper(c?.$2, newValue.$2); + }); + } + } + }, + customBorder: widget.style(widget.configuration.isEnabled).border, + highlightColor: Colors.transparent, + ), + ), + ) + ], + ), + ), + ), + widget.messageBuilder != null && + widget.messageBuilder!( + widget.configuration.isEnabled, + ) != + null + ? Padding( + padding: widget.style(widget.configuration.isEnabled).messagePadding, + child: widget.messageBuilder!(widget.configuration.isEnabled) ?? Container(), + ) + : Container() + ], + ); + //!SECTION + } + + @override + void dispose() { + //SECTION - Disposable variables + //!SECTION + super.dispose(); + } +}