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:
@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user