mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: composing in select option text field doesn't work (#4470)
* fix: composing in select option text field doesn't work * style: improve code style
This commit is contained in:
parent
3f896ad64f
commit
e88fb533c8
@ -11,7 +11,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../../../grid/presentation/widgets/common/type_option_separator.dart';
|
||||
@ -32,13 +31,12 @@ class SelectOptionCellEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final popoverMutex = PopoverMutex();
|
||||
final tagController = TextfieldTagsController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
tagController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -54,14 +52,14 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TextField(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
tagController: tagController,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Flexible(
|
||||
child: _OptionList(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
tagController: tagController,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -73,12 +71,12 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
}
|
||||
|
||||
class _OptionList extends StatelessWidget {
|
||||
final TextEditingController textEditingController;
|
||||
final PopoverMutex popoverMutex;
|
||||
final TextfieldTagsController tagController;
|
||||
|
||||
const _OptionList({
|
||||
required this.textEditingController,
|
||||
required this.popoverMutex,
|
||||
required this.tagController,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -117,23 +115,23 @@ class _OptionList extends StatelessWidget {
|
||||
}
|
||||
|
||||
void onPressedAddButton(BuildContext context) {
|
||||
final text = tagController.textEditingController?.text;
|
||||
if (text != null) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.trySelectOption(text),
|
||||
);
|
||||
final text = textEditingController.text;
|
||||
if (text.isNotEmpty) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(text));
|
||||
}
|
||||
tagController.textEditingController?.clear();
|
||||
textEditingController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _TextField extends StatelessWidget {
|
||||
final TextEditingController textEditingController;
|
||||
final PopoverMutex popoverMutex;
|
||||
final TextfieldTagsController tagController;
|
||||
|
||||
const _TextField({
|
||||
required this.textEditingController,
|
||||
required this.popoverMutex,
|
||||
required this.tagController,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -152,7 +150,7 @@ class _TextField extends StatelessWidget {
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
tagController: tagController,
|
||||
textController: textEditingController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
|
@ -6,17 +6,16 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
|
||||
class SelectOptionTextField extends StatefulWidget {
|
||||
final TextfieldTagsController tagController;
|
||||
final List<SelectOptionPB> options;
|
||||
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
|
||||
final double distanceToText;
|
||||
final List<String> textSeparators;
|
||||
final TextEditingController? textController;
|
||||
final TextEditingController textController;
|
||||
|
||||
final Function(String) onSubmitted;
|
||||
final Function(String) newText;
|
||||
@ -29,13 +28,12 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
required this.options,
|
||||
required this.selectedOptionMap,
|
||||
required this.distanceToText,
|
||||
required this.tagController,
|
||||
required this.onSubmitted,
|
||||
required this.onPaste,
|
||||
required this.onRemove,
|
||||
required this.newText,
|
||||
required this.textSeparators,
|
||||
this.textController,
|
||||
required this.textController,
|
||||
this.onClick,
|
||||
});
|
||||
|
||||
@ -44,103 +42,105 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
final FocusNode focusNode = FocusNode();
|
||||
late final TextEditingController controller;
|
||||
late final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = widget.textController ?? TextEditingController();
|
||||
focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
if (!widget.textController.value.composing.isCollapsed) {
|
||||
final TextRange(:start, :end) =
|
||||
widget.textController.value.composing;
|
||||
final text = widget.textController.text;
|
||||
|
||||
widget.textController.value = TextEditingValue(
|
||||
text: "${text.substring(0, start)}${text.substring(end)}",
|
||||
selection: TextSelection(baseOffset: start, extentOffset: start),
|
||||
composing: const TextRange(start: -1, end: -1),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
});
|
||||
widget.textController.addListener(_onChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textController.removeListener(_onChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextFieldTags(
|
||||
textEditingController: controller,
|
||||
textfieldTagsController: widget.tagController,
|
||||
initialTags: widget.selectedOptionMap.keys.toList(),
|
||||
return TextField(
|
||||
controller: widget.textController,
|
||||
focusNode: focusNode,
|
||||
textSeparators: widget.textSeparators,
|
||||
inputfieldBuilder: (
|
||||
BuildContext context,
|
||||
editController,
|
||||
focusNode,
|
||||
error,
|
||||
onChanged,
|
||||
onSubmitted,
|
||||
) {
|
||||
return ((context, sc, tags, onTagDelegate) {
|
||||
return TextField(
|
||||
controller: editController,
|
||||
focusNode: focusNode,
|
||||
onTap: widget.onClick,
|
||||
onChanged: (text) {
|
||||
if (onChanged != null) {
|
||||
onChanged(text);
|
||||
}
|
||||
_newText(text, editController);
|
||||
},
|
||||
onSubmitted: (text) {
|
||||
if (onSubmitted != null) {
|
||||
onSubmitted(text);
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
widget.onSubmitted(text.trim());
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(context, sc),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).hintColor),
|
||||
prefixIconConstraints:
|
||||
BoxConstraints(maxWidth: widget.distanceToText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
onTap: widget.onClick,
|
||||
onSubmitted: (text) {
|
||||
if (text.isNotEmpty) {
|
||||
widget.onSubmitted(text.trim());
|
||||
focusNode.requestFocus();
|
||||
widget.textController.clear();
|
||||
}
|
||||
},
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(context),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).hintColor),
|
||||
prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _newText(String text, TextEditingController editingController) {
|
||||
if (text.isEmpty) {
|
||||
widget.newText('');
|
||||
void _onChanged() {
|
||||
if (!widget.textController.value.composing.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result = splitInput(text.trimLeft(), widget.textSeparators);
|
||||
// split input
|
||||
final (submitted, remainder) = splitInput(
|
||||
widget.textController.text.trimLeft(),
|
||||
widget.textSeparators,
|
||||
);
|
||||
|
||||
editingController.text = result[1];
|
||||
editingController.selection =
|
||||
TextSelection.collapsed(offset: controller.text.length);
|
||||
widget.onPaste(result[0], result[1]);
|
||||
if (submitted.isNotEmpty) {
|
||||
widget.textController.text = remainder;
|
||||
widget.textController.selection =
|
||||
TextSelection.collapsed(offset: widget.textController.text.length);
|
||||
}
|
||||
widget.onPaste(submitted, remainder);
|
||||
}
|
||||
|
||||
Widget? _renderTags(BuildContext context, ScrollController sc) {
|
||||
Widget? _renderTags(BuildContext context) {
|
||||
if (widget.selectedOptionMap.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
@ -169,7 +169,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
controller: sc,
|
||||
controller: ScrollController(),
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
),
|
||||
@ -180,7 +180,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
List splitInput(String input, List<String> textSeparators) {
|
||||
(List<String>, String) splitInput(String input, List<String> textSeparators) {
|
||||
final List<String> splits = [];
|
||||
String currentString = '';
|
||||
|
||||
@ -201,5 +201,5 @@ List splitInput(String input, List<String> textSeparators) {
|
||||
final submittedOptions = splits.sublist(0, splits.length - 1).toList();
|
||||
final remainder = splits.elementAt(splits.length - 1).trimLeft();
|
||||
|
||||
return [submittedOptions, remainder];
|
||||
return (submittedOptions, remainder);
|
||||
}
|
||||
|
@ -1872,14 +1872,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.9"
|
||||
textfield_tags:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: textfield_tags
|
||||
sha256: c1d215f481e7e8da5c79719825e595db4f829bf1ad3fce4c7ce43d340aa72683
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
textstyle_extensions:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -74,7 +74,6 @@ dependencies:
|
||||
clipboard: ^0.1.3
|
||||
connectivity_plus: ^5.0.2
|
||||
easy_localization: ^3.0.2
|
||||
textfield_tags: ^2.0.2
|
||||
device_info_plus: ^9.0.1
|
||||
fluttertoast: ^8.2.2
|
||||
json_annotation: ^4.8.1
|
||||
|
@ -6,41 +6,43 @@ void main() {
|
||||
|
||||
group('split input unit test', () {
|
||||
test('empty input', () {
|
||||
List result = splitInput(' ', textSeparators);
|
||||
expect(result[0], []);
|
||||
expect(result[1], '');
|
||||
var (submitted, remainder) = splitInput(' ', textSeparators);
|
||||
expect(submitted, []);
|
||||
expect(remainder, '');
|
||||
|
||||
result = splitInput(', , , ', textSeparators);
|
||||
expect(result[0], []);
|
||||
expect(result[1], '');
|
||||
(submitted, remainder) = splitInput(', , , ', textSeparators);
|
||||
expect(submitted, []);
|
||||
expect(remainder, '');
|
||||
});
|
||||
|
||||
test('simple input', () {
|
||||
List result = splitInput('exampleTag', textSeparators);
|
||||
expect(result[0], []);
|
||||
expect(result[1], 'exampleTag');
|
||||
var (submitted, remainder) = splitInput('exampleTag', textSeparators);
|
||||
expect(submitted, []);
|
||||
expect(remainder, 'exampleTag');
|
||||
|
||||
result = splitInput('tag with longer name', textSeparators);
|
||||
expect(result[0], []);
|
||||
expect(result[1], 'tag with longer name');
|
||||
(submitted, remainder) =
|
||||
splitInput('tag with longer name', textSeparators);
|
||||
expect(submitted, []);
|
||||
expect(remainder, 'tag with longer name');
|
||||
|
||||
result = splitInput('trailing space ', textSeparators);
|
||||
expect(result[0], []);
|
||||
expect(result[1], 'trailing space ');
|
||||
(submitted, remainder) = splitInput('trailing space ', textSeparators);
|
||||
expect(submitted, []);
|
||||
expect(remainder, 'trailing space ');
|
||||
});
|
||||
|
||||
test('input with commas', () {
|
||||
List result = splitInput('a, b, c', textSeparators);
|
||||
expect(result[0], ['a', 'b']);
|
||||
expect(result[1], 'c');
|
||||
var (submitted, remainder) = splitInput('a, b, c', textSeparators);
|
||||
expect(submitted, ['a', 'b']);
|
||||
expect(remainder, 'c');
|
||||
|
||||
result = splitInput('a, b, c, ', textSeparators);
|
||||
expect(result[0], ['a', 'b', 'c']);
|
||||
expect(result[1], '');
|
||||
(submitted, remainder) = splitInput('a, b, c, ', textSeparators);
|
||||
expect(submitted, ['a', 'b', 'c']);
|
||||
expect(remainder, '');
|
||||
|
||||
result = splitInput(',tag 1 ,2nd tag, third tag ', textSeparators);
|
||||
expect(result[0], ['tag 1', '2nd tag']);
|
||||
expect(result[1], 'third tag ');
|
||||
(submitted, remainder) =
|
||||
splitInput(',tag 1 ,2nd tag, third tag ', textSeparators);
|
||||
expect(submitted, ['tag 1', '2nd tag']);
|
||||
expect(remainder, 'third tag ');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/t
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
import '../bloc_test/grid_test/util.dart';
|
||||
|
||||
@ -22,7 +21,6 @@ void main() {
|
||||
options: const [],
|
||||
selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),
|
||||
distanceToText: 0.0,
|
||||
tagController: TextfieldTagsController(),
|
||||
onSubmitted: (text) => submit = text,
|
||||
onPaste: (options, remaining) {
|
||||
remainder = remaining;
|
||||
|
Loading…
Reference in New Issue
Block a user