import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:invoiceninja_flutter/data/models/models.dart'; import 'package:invoiceninja_flutter/redux/app/app_state.dart'; import 'package:invoiceninja_flutter/ui/app/responsive_padding.dart'; import 'package:invoiceninja_flutter/utils/formatting.dart'; import 'package:invoiceninja_flutter/utils/localization.dart'; import 'package:invoiceninja_flutter/utils/platforms.dart'; import 'package:invoiceninja_flutter/.env.dart'; class EntityDropdown extends StatefulWidget { const EntityDropdown({ @required Key key, @required this.entityType, @required this.labelText, @required this.onSelected, this.entityMap, this.entityList, this.allowClearing = false, this.autoValidate = false, this.validator, this.entityId, this.onAddPressed, this.autofocus = false, this.onFieldSubmitted, }) : super(key: key); final EntityType entityType; final List entityList; final String labelText; final String entityId; final bool autofocus; final BuiltMap entityMap; final Function(SelectableEntity) onSelected; final Function validator; final bool autoValidate; final bool allowClearing; final Function(String) onFieldSubmitted; final Function(Completer completer) onAddPressed; @override _EntityDropdownState createState() => _EntityDropdownState(); } class _EntityDropdownState extends State { final _textController = TextEditingController(); final _focusNode = FocusNode(); BuiltMap _entityMap; @override void initState() { super.initState(); _focusNode.addListener(() { if (_focusNode.hasFocus) { _showOptions(); } }); } @override void didChangeDependencies() { final state = StoreProvider.of(context).state; _entityMap = widget.entityMap ?? state.getEntityMap(widget.entityType); if (_entityMap == null) { print('ERROR: ENTITY MAP IS NULL: ${widget.entityType}'); } else { _textController.text = _entityMap[widget.entityId]?.listDisplayName ?? ''; } super.didChangeDependencies(); } @override void dispose() { _textController.dispose(); _focusNode.dispose(); super.dispose(); } void _showOptions() { showDialog( context: context, builder: (BuildContext context) { return EntityDropdownDialog( entityMap: _entityMap, entityList: widget.entityList ?? _entityMap.keys.toList(), onSelected: (entity, [update = true]) { widget.onSelected(entity); if (update) { _textController.text = entity.listDisplayName; } if (widget.onFieldSubmitted != null) { widget.onFieldSubmitted(entity.listDisplayName); } }, onAddPressed: widget.onAddPressed != null ? (context, completer) => widget.onAddPressed(completer) : null, ); }); } bool get showClear => widget.allowClearing && widget.entityId != null && widget.entityId.isNotEmpty; @override Widget build(BuildContext context) { // TODO remove DEMO_MODE check if (isNotMobile(context) && !Config.DEMO_MODE) { return Stack( alignment: Alignment.centerRight, children: [ TypeAheadFormField( noItemsFoundBuilder: (context) => SizedBox(), suggestionsBoxDecoration: SuggestionsBoxDecoration( constraints: BoxConstraints( minWidth: 300, ), ), suggestionsCallback: (filter) { return (widget.entityList ?? widget.entityMap.keys.toList()) .where((entityId) => _entityMap[entityId]?.matchesFilter(filter) ?? false) .toList(); }, itemBuilder: (context, entityId) { // TODO remove this /* return _EntityListTile( entity: _entityMap[entityId], filter: _textController.text, ); */ return Listener( child: Container( color: Theme.of(context).cardColor, child: _EntityListTile( entity: _entityMap[entityId], filter: _textController.text, ), ), onPointerDown: (_) { final entity = _entityMap[entityId]; _textController.text = _entityMap[entityId].listDisplayName; widget.onSelected(entity); }, ); }, onSuggestionSelected: (entityId) { final entity = _entityMap[entityId]; _textController.text = _entityMap[entityId].listDisplayName; widget.onSelected(entity); }, textFieldConfiguration: TextFieldConfiguration( controller: _textController, autofocus: widget.autofocus ?? false, decoration: InputDecoration( labelText: widget.labelText, ), ), //direction: AxisDirection.up, autoFlipDirection: true, animationStart: 1, debounceDuration: Duration(seconds: 0), ), showClear ? IconButton( icon: Icon(Icons.clear), onPressed: () { _textController.text = ''; widget.onSelected(null); }, ) : widget.onAddPressed != null ? IconButton( icon: Icon(Icons.add_circle_outline), tooltip: AppLocalization.of(context).createNew, onPressed: () { final Completer completer = Completer(); widget.onAddPressed(completer); completer.future.then( (entity) { widget.onSelected(entity); }, ); }, ) : SizedBox(), ], ); } return Stack( alignment: Alignment.centerRight, children: [ InkWell( //key: ValueKey('__stack_${widget.labelText}__'), onTap: () => _showOptions(), child: IgnorePointer( child: TextFormField( focusNode: _focusNode, readOnly: true, validator: widget.validator, autovalidate: widget.autoValidate, controller: _textController, decoration: InputDecoration( labelText: widget.labelText, suffixIcon: showClear ? null : const Icon(Icons.search), ), ), ), ), if (showClear) IconButton( icon: Icon(Icons.clear), onPressed: () { _textController.text = ''; widget.onSelected(null); }, ), ], ); } } class EntityDropdownDialog extends StatefulWidget { const EntityDropdownDialog({ @required this.entityMap, @required this.entityList, @required this.onSelected, this.onAddPressed, }); final BuiltMap entityMap; final List entityList; final Function(SelectableEntity, [bool]) onSelected; final Function(BuildContext context, Completer completer) onAddPressed; @override _EntityDropdownDialogState createState() => _EntityDropdownDialogState(); } class _EntityDropdownDialogState extends State { String _filter; @override Widget build(BuildContext context) { final localization = AppLocalization.of(context); void _selectEntity(SelectableEntity entity) { widget.onSelected(entity); Navigator.pop(context); } Widget _headerRow() { return Row( children: [ Padding( padding: const EdgeInsets.only(left: 10.0, right: 10.0), child: Icon( Icons.search, color: Colors.grey, ), ), Expanded( child: TextField( /* onSubmitted: (value) { final entityId = widget.entityList.firstWhere((entityId) => _entityMap[entityId].matchesFilter(_filter)); final entity = _entityMap[entityId]; _selectEntity(entity); }, */ onChanged: (value) { setState(() { _filter = value; }); }, autofocus: true, decoration: InputDecoration( border: InputBorder.none, hintText: localization.filter, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), ), widget.onAddPressed != null ? IconButton( icon: Icon(Icons.add_circle_outline), tooltip: localization.createNew, onPressed: () { Navigator.pop(context); final Completer completer = Completer(); widget.onAddPressed(context, completer); completer.future.then((entity) { widget.onSelected(entity, false); }); }, ) : Container() ], ); } Widget _createList() { final matches = widget.entityList .where((entityId) => widget.entityMap[entityId]?.matchesFilter(_filter) ?? false) .toList(); return ListView.builder( shrinkWrap: true, itemCount: matches.length, itemBuilder: (BuildContext context, int index) { final entityId = matches[index]; final entity = widget.entityMap[entityId]; return _EntityListTile( entity: entity, filter: _filter, onTap: (entity) => _selectEntity(entity), ); }, ); } return ResponsivePadding( child: Material( elevation: 4.0, child: Column( mainAxisSize: MainAxisSize.min, children: [ _headerRow(), Expanded(child: _createList()), ], ), ), ); } } class _EntityListTile extends StatelessWidget { const _EntityListTile({ @required this.entity, @required this.filter, this.onTap, }); final SelectableEntity entity; final Function(SelectableEntity entity) onTap; final String filter; @override Widget build(BuildContext context) { final String subtitle = entity.matchesFilterValue(filter); return ListTile( title: Row( children: [ Expanded( child: Text(entity.listDisplayName, style: Theme.of(context).textTheme.title), ), entity.listDisplayAmount != null ? Text( formatNumber(entity.listDisplayAmount, context, formatNumberType: entity.listDisplayAmountType), style: Theme.of(context).textTheme.title) : Container(), ], ), subtitle: subtitle != null ? Text(subtitle, maxLines: 2) : null, onTap: onTap != null ? () => onTap(entity) : null, ); } }