diff --git a/.gitignore b/.gitignore index ed90a429..b4255e56 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,5 @@ pubspec.lock # Old files *.old + +.DS_Store diff --git a/lib/src/chips_input.dart b/lib/src/chips_input.dart index 3c10a20d..cd7df440 100644 --- a/lib/src/chips_input.dart +++ b/lib/src/chips_input.dart @@ -9,8 +9,7 @@ import 'text_cursor.dart'; typedef ChipsInputSuggestions = FutureOr> Function(String query); typedef ChipSelected = void Function(T data, bool selected); -typedef ChipsBuilder = Widget Function( - BuildContext context, ChipsInputState state, T data); +typedef ChipsBuilder = Widget Function(BuildContext context, ChipsInputState state, T data); const kObjectReplacementChar = 0xFFFD; @@ -19,9 +18,7 @@ extension on TextEditingValue { text.codeUnits.where((ch) => ch != kObjectReplacementChar), ); - List get replacementCharacters => text.codeUnits - .where((ch) => ch == kObjectReplacementChar) - .toList(growable: false); + List get replacementCharacters => text.codeUnits.where((ch) => ch == kObjectReplacementChar).toList(growable: false); int get replacementCharactersCount => replacementCharacters.length; } @@ -84,12 +81,10 @@ class ChipsInput extends StatefulWidget { ChipsInputState createState() => ChipsInputState(); } -class ChipsInputState extends State> - implements TextInputClient { +class ChipsInputState extends State> with TextInputClient { Set _chips = {}; List? _suggestions; - final StreamController?> _suggestionsStreamController = - StreamController?>.broadcast(); + final StreamController?> _suggestionsStreamController = StreamController?>.broadcast(); int _searchId = 0; TextEditingValue _value = const TextEditingValue(); TextInputConnection? _textInputConnection; @@ -107,15 +102,12 @@ class ChipsInputState extends State> textCapitalization: widget.textCapitalization, ); - bool get _hasInputConnection => - _textInputConnection != null && _textInputConnection!.attached; + bool get _hasInputConnection => _textInputConnection != null && _textInputConnection!.attached; - bool get _hasReachedMaxChips => - widget.maxChips != null && _chips.length >= widget.maxChips!; + bool get _hasReachedMaxChips => widget.maxChips != null && _chips.length >= widget.maxChips!; FocusNode? _focusNode; - FocusNode get _effectiveFocusNode => - widget.focusNode ?? (_focusNode ??= FocusNode()); + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); late FocusAttachment _nodeAttachment; RenderBox? get renderBox => context.findRenderObject() as RenderBox?; @@ -126,9 +118,7 @@ class ChipsInputState extends State> void initState() { super.initState(); _chips.addAll(widget.initialValue); - _suggestions = widget.initialSuggestions - ?.where((r) => !_chips.contains(r)) - .toList(growable: false); + _suggestions = widget.initialSuggestions?.where((r) => !_chips.contains(r)).toList(growable: false); _suggestionsBoxController = SuggestionsBoxController(context); _effectiveFocusNode.addListener(_handleFocusChanged); @@ -183,19 +173,14 @@ class ChipsInputState extends State> final renderBoxOffset = renderBox!.localToGlobal(Offset.zero); final topAvailableSpace = renderBoxOffset.dy; final mq = MediaQuery.of(context); - final bottomAvailableSpace = mq.size.height - - mq.viewInsets.bottom - - renderBoxOffset.dy - - size.height; + final bottomAvailableSpace = mq.size.height - mq.viewInsets.bottom - renderBoxOffset.dy - size.height; var suggestionBoxHeight = max(topAvailableSpace, bottomAvailableSpace); if (null != widget.suggestionsBoxMaxHeight) { - suggestionBoxHeight = - min(suggestionBoxHeight, widget.suggestionsBoxMaxHeight!); + suggestionBoxHeight = min(suggestionBoxHeight, widget.suggestionsBoxMaxHeight!); } final showTop = topAvailableSpace > bottomAvailableSpace; // print("showTop: $showTop" ); - final compositedTransformFollowerOffset = - showTop ? Offset(0, -size.height) : Offset.zero; + final compositedTransformFollowerOffset = showTop ? Offset(0, -size.height) : Offset.zero; return StreamBuilder?>( stream: _suggestionsStreamController.stream, @@ -288,7 +273,7 @@ class ChipsInputState extends State> Future.delayed(const Duration(milliseconds: 300), () { WidgetsBinding.instance.addPostFrameCallback((_) async { final renderBox = context.findRenderObject() as RenderBox; - await Scrollable.of(context)?.position.ensureVisible(renderBox); + await Scrollable.of(context).position.ensureVisible(renderBox); }); }); } @@ -297,8 +282,7 @@ class ChipsInputState extends State> final localId = ++_searchId; final results = await widget.findSuggestions(value); if (_searchId == localId && mounted) { - setState(() => _suggestions = - results.where((r) => !_chips.contains(r)).toList(growable: false)); + setState(() => _suggestions = results.where((r) => !_chips.contains(r)).toList(growable: false)); } _suggestionsStreamController.add(_suggestions ?? []); if (!_suggestionsBoxController.isOpened && !_hasReachedMaxChips) { @@ -320,11 +304,9 @@ class ChipsInputState extends State> final oldTextEditingValue = _value; if (value.text != oldTextEditingValue.text) { setState(() => _value = value); - if (value.replacementCharactersCount < - oldTextEditingValue.replacementCharactersCount) { + if (value.replacementCharactersCount < oldTextEditingValue.replacementCharactersCount && _chips.isNotEmpty) { final removedChip = _chips.last; - setState(() => - _chips = Set.of(_chips.take(value.replacementCharactersCount))); + setState(() => _chips = Set.of(_chips.take(value.replacementCharactersCount))); widget.onChanged(_chips.toList(growable: false)); String? putText = ''; if (widget.allowChipEditing && _enteredTexts.containsKey(removedChip)) { @@ -332,8 +314,6 @@ class ChipsInputState extends State> _enteredTexts.remove(removedChip); } _updateTextInputState(putText: putText); - } else { - _updateTextInputState(); } _onSearchChanged(_value.normalCharactersText); } @@ -342,9 +322,7 @@ class ChipsInputState extends State> void _updateTextInputState({replaceText = false, putText = ''}) { if (replaceText || putText != '') { final updatedText = - String.fromCharCodes(_chips.map((_) => kObjectReplacementChar)) + - (replaceText ? '' : _value.normalCharactersText) + - putText; + String.fromCharCodes(_chips.map((_) => kObjectReplacementChar)) + (replaceText ? '' : _value.normalCharactersText) + putText; setState(() => _value = _value.copyWith( text: updatedText, selection: TextSelection.collapsed(offset: updatedText.length), @@ -355,6 +333,7 @@ class ChipsInputState extends State> _closeInputConnectionIfNeeded(); //Hack for #34 (https://github.com/danvick/flutter_chips_input/issues/34#issuecomment-684505282). TODO: Find permanent fix _textInputConnection ??= TextInput.attach(this, textInputConfiguration); _textInputConnection?.setEditingState(_value); + _textInputConnection?.show(); } @override @@ -415,9 +394,7 @@ class ChipsInputState extends State> @override Widget build(BuildContext context) { _nodeAttachment.reparent(); - final chipsChildren = _chips - .map((data) => widget.chipBuilder(context, this, data)) - .toList(); + final chipsChildren = _chips.map((data) => widget.chipBuilder(context, this, data)).toList(); final theme = Theme.of(context); @@ -434,8 +411,7 @@ class ChipsInputState extends State> _value.normalCharactersText, maxLines: 1, overflow: widget.textOverflow, - style: widget.textStyle ?? - theme.textTheme.subtitle1!.copyWith(height: 1.5), + style: widget.textStyle ?? theme.textTheme.titleMedium!.copyWith(height: 1.5), ), ), Flexible( @@ -447,38 +423,48 @@ class ChipsInputState extends State> ), ); - return NotificationListener( - onNotification: (SizeChangedLayoutNotification val) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - _suggestionsBoxController.overlayEntry?.markNeedsBuild(); - }); - return true; + return RawKeyboardListener( + focusNode: _focusNode!, // or FocusNode() + onKey: (event) { + final str = currentTextEditingValue.text; + if (event.runtimeType.toString() == 'RawKeyDownEvent' && event.logicalKey == LogicalKeyboardKey.backspace && str.isNotEmpty) { + final sd = str.substring(0, str.length - 1); + updateEditingValue(TextEditingValue(text: sd, selection: TextSelection.collapsed(offset: sd.length))); + } }, - child: SizeChangedLayoutNotifier( - child: Column( - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - requestKeyboard(); - }, - child: InputDecorator( - decoration: widget.decoration, - isFocused: _effectiveFocusNode.hasFocus, - isEmpty: _value.text.isEmpty && _chips.isEmpty, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 4.0, - runSpacing: 4.0, - children: chipsChildren, + child: NotificationListener( + onNotification: (SizeChangedLayoutNotification val) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + _suggestionsBoxController.overlayEntry?.markNeedsBuild(); + }); + return true; + }, + child: SizeChangedLayoutNotifier( + child: Column( + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + requestKeyboard(); + }, + child: InputDecorator( + decoration: widget.decoration, + isFocused: _effectiveFocusNode.hasFocus, + isEmpty: _value.text.isEmpty && _chips.isEmpty, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4.0, + runSpacing: 4.0, + children: chipsChildren, + ), ), ), - ), - CompositedTransformTarget( - link: _layerLink, - child: Container(), - ), - ], + CompositedTransformTarget( + link: _layerLink, + child: Container(), + ), + ], + ), ), ), ); diff --git a/lib/src/suggestions_box_controller.dart b/lib/src/suggestions_box_controller.dart index 127c7452..157d55eb 100644 --- a/lib/src/suggestions_box_controller.dart +++ b/lib/src/suggestions_box_controller.dart @@ -14,7 +14,7 @@ class SuggestionsBoxController { void open() { if (_isOpened) return; assert(overlayEntry != null); - Overlay.of(context)!.insert(overlayEntry!); + Overlay.of(context).insert(overlayEntry!); _isOpened = true; } diff --git a/lib/src/text_cursor.dart b/lib/src/text_cursor.dart index e00da661..e8a69d9f 100644 --- a/lib/src/text_cursor.dart +++ b/lib/src/text_cursor.dart @@ -13,11 +13,10 @@ class TextCursor extends StatefulWidget { final bool resumed; @override - _TextCursorState createState() => _TextCursorState(); + State createState() => _TextCursorState(); } -class _TextCursorState extends State - with SingleTickerProviderStateMixin { +class _TextCursorState extends State with SingleTickerProviderStateMixin { bool _displayed = false; late Timer _timer; @@ -46,9 +45,7 @@ class _TextCursorState extends State opacity: _displayed && widget.resumed ? 1.0 : 0.0, child: Container( width: 2.0, - color: theme.textSelectionTheme.cursorColor ?? - theme.textSelectionTheme.selectionColor ?? - theme.primaryColor, + color: theme.textSelectionTheme.cursorColor ?? theme.textSelectionTheme.selectionColor ?? theme.primaryColor, ), ), );