mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: select option cell editor revamp (#5011)
* chore: gen new select option color on frontend * chore: reorder select options * chore: fix performance regression * chore: add text field tap region * chore: implement hover focus * chore: implement keyboard focus * chore: fix tests * chore: reorder options in field editor * chore: fix tests
This commit is contained in:
@ -7,6 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -16,29 +17,29 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
||||
constraints: const BoxConstraints.tightFor(width: 300),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cellContainerNotifier.isFocus = true;
|
||||
});
|
||||
return SelectOptionCellEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -18,12 +19,11 @@ class DesktopRowDetailSelectOptionCellSkin
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
||||
constraints: const BoxConstraints.tightFor(width: 300),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
@ -35,14 +35,18 @@ class DesktopRowDetailSelectOptionCellSkin
|
||||
);
|
||||
},
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: state.selectedOptions.isEmpty
|
||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
||||
: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: state.selectedOptions.isEmpty
|
||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
||||
: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ abstract class IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
);
|
||||
}
|
||||
@ -77,16 +76,11 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
state,
|
||||
_popover,
|
||||
);
|
||||
},
|
||||
child: widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
_popover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -17,25 +18,28 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return FlowyButton(
|
||||
hoverColor: Colors.transparent,
|
||||
radius: BorderRadius.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
text: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return FlowyButton(
|
||||
hoverColor: Colors.transparent,
|
||||
radius: BorderRadius.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
text: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -20,53 +21,56 @@ class MobileRowDetailSelectOptionCellSkin
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: state.selectedOptions.isEmpty ? 13 : 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
const HSpace(6),
|
||||
RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).hintColor,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: state.selectedOptions.isEmpty ? 13 : 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
const HSpace(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
const HSpace(6),
|
||||
RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ extension SelectOptionColorExtension on SelectOptionColorPB {
|
||||
}
|
||||
}
|
||||
|
||||
String optionName() {
|
||||
String colorName() {
|
||||
switch (this) {
|
||||
case SelectOptionColorPB.Purple:
|
||||
return LocaleKeys.grid_selectOption_purpleColor.tr();
|
||||
@ -123,44 +123,3 @@ class SelectOptionTag extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTagCell extends StatelessWidget {
|
||||
const SelectOptionTagCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.onSelected,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final VoidCallback onSelected;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onSelected,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import 'package:appflowy/mobile/presentation/base/option_color_list.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
@ -55,8 +55,9 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
child: BlocProvider(
|
||||
create: (context) => SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc,
|
||||
SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -110,7 +111,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
onDelete: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(option!));
|
||||
.add(SelectOptionCellEditorEvent.deleteOption(option!));
|
||||
_popOrBack();
|
||||
},
|
||||
onUpdate: (name, color) {
|
||||
@ -120,7 +121,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
}
|
||||
option.freeze();
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.updateOption(
|
||||
SelectOptionCellEditorEvent.updateOption(
|
||||
option.rebuild((p0) {
|
||||
if (name != null) {
|
||||
p0.name = name;
|
||||
@ -142,16 +143,16 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
_SearchField(
|
||||
controller: searchController,
|
||||
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
||||
onSubmitted: (option) {
|
||||
onSubmitted: (_) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(option));
|
||||
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
searchController.clear();
|
||||
},
|
||||
onChanged: (value) {
|
||||
typingOption = value;
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.selectMultipleOptions(
|
||||
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
[],
|
||||
value,
|
||||
),
|
||||
@ -164,18 +165,18 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
onCreateOption: (optionName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(optionName));
|
||||
.add(const SelectOptionCellEditorEvent.createOption());
|
||||
searchController.clear();
|
||||
},
|
||||
onCheck: (option, value) {
|
||||
if (value) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
.add(SelectOptionCellEditorEvent.selectOption(option.id));
|
||||
} else {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.unSelectOption(option.id));
|
||||
.add(SelectOptionCellEditorEvent.unSelectOption(option.id));
|
||||
}
|
||||
},
|
||||
onMoreOptions: (option) {
|
||||
@ -253,18 +254,20 @@ class _OptionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
// existing options
|
||||
final List<Widget> cells = [];
|
||||
|
||||
// create an option cell
|
||||
final createOption = state.createOption;
|
||||
if (createOption != null) {
|
||||
if (state.createSelectOptionSuggestion != null) {
|
||||
cells.add(
|
||||
_CreateOptionCell(
|
||||
optionName: createOption,
|
||||
onTap: () => onCreateOption(createOption),
|
||||
name: state.createSelectOptionSuggestion!.name,
|
||||
color: state.createSelectOptionSuggestion!.color,
|
||||
onTap: () => onCreateOption(
|
||||
state.createSelectOptionSuggestion!.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -332,14 +335,17 @@ class _SelectOption extends StatelessWidget {
|
||||
const HSpace(12),
|
||||
// option tag
|
||||
Expanded(
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: 15.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: 15.0,
|
||||
isExpanded: true,
|
||||
),
|
||||
),
|
||||
const HSpace(24),
|
||||
@ -359,11 +365,13 @@ class _SelectOption extends StatelessWidget {
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.optionName,
|
||||
required this.name,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String optionName;
|
||||
final String name;
|
||||
final SelectOptionColorPB color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
@ -381,13 +389,16 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: SelectOptionTag(
|
||||
isExpanded: true,
|
||||
name: optionName,
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
textAlign: TextAlign.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectOptionTag(
|
||||
name: name,
|
||||
color: color.toColor(context),
|
||||
textAlign: TextAlign.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,18 +1,20 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../../grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import '../field/type_option_editor/select/select_option_editor.dart';
|
||||
@ -33,39 +35,81 @@ class SelectOptionCellEditor extends StatefulWidget {
|
||||
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final popoverMutex = PopoverMutex();
|
||||
late final bloc = SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
);
|
||||
late final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyUpEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
switch (event.logicalKey) {
|
||||
case LogicalKeyboardKey.arrowUp:
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption());
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
case LogicalKeyboardKey.arrowDown:
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(const SelectOptionCellEditorEvent.focusNextOption());
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
case LogicalKeyboardKey.escape:
|
||||
if (!textEditingController.value.composing.isCollapsed) {
|
||||
final end = textEditingController.value.composing.end;
|
||||
final text = textEditingController.text;
|
||||
|
||||
textEditingController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: end),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
textEditingController.dispose();
|
||||
bloc.close();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TextField(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Flexible(
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: TextFieldTapRegion(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TextField(
|
||||
textEditingController: textEditingController,
|
||||
focusNode: focusNode,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Flexible(
|
||||
child: Focus(
|
||||
descendantsAreFocusable: false,
|
||||
child: _OptionList(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -82,60 +126,83 @@ class _OptionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
buildWhen: (previous, current) =>
|
||||
!listEquals(previous.options, current.options) ||
|
||||
previous.createSelectOptionSuggestion !=
|
||||
current.createSelectOptionSuggestion,
|
||||
builder: (context, state) {
|
||||
final cells = [
|
||||
_Title(onPressedAddButton: () => onPressedAddButton(context)),
|
||||
...state.options.map(
|
||||
(option) => _SelectOptionCell(
|
||||
option: option,
|
||||
isSelected: state.selectedOptions.contains(option),
|
||||
popoverMutex: popoverMutex,
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<SelectOptionCellEditorBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final createOption = state.createOption;
|
||||
if (createOption != null) {
|
||||
cells.add(_CreateOptionCell(name: createOption));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, int index) => cells[index],
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: state.options.length,
|
||||
onReorderStart: (_) => popoverMutex.close(),
|
||||
itemBuilder: (_, int index) {
|
||||
final option = state.options[index];
|
||||
return _SelectOptionCell(
|
||||
key: ValueKey("select_cell_option_list_${option.id}"),
|
||||
index: index,
|
||||
option: option,
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromOptionId = state.options[oldIndex].id;
|
||||
final toOptionId = state.options[newIndex].id;
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.reorderOption(
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
header: const _Title(),
|
||||
footer: state.createSelectOptionSuggestion == null
|
||||
? null
|
||||
: _CreateOptionCell(
|
||||
suggestion: state.createSelectOptionSuggestion!,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onPressedAddButton(BuildContext context) {
|
||||
final text = textEditingController.text;
|
||||
if (text.isNotEmpty) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(text));
|
||||
}
|
||||
textEditingController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _TextField extends StatelessWidget {
|
||||
const _TextField({
|
||||
required this.textEditingController,
|
||||
required this.focusNode,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
final FocusNode focusNode;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
final optionMap = LinkedHashMap<String, SelectOptionPB>.fromIterable(
|
||||
state.selectedOptions,
|
||||
@ -143,40 +210,46 @@ class _TextField extends StatelessWidget {
|
||||
value: (option) => option,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
textController: textEditingController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (tagName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
),
|
||||
);
|
||||
},
|
||||
onRemove: (optionName) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.unSelectOption(
|
||||
optionMap[optionName]!.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
focusNode: focusNode,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
textController: textEditingController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionCellEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
textEditingController.clear();
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
),
|
||||
);
|
||||
},
|
||||
onRemove: (optionName) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.unSelectOption(
|
||||
optionMap[optionName]!.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -185,11 +258,7 @@ class _TextField extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
const _Title({
|
||||
required this.onPressedAddButton,
|
||||
});
|
||||
|
||||
final VoidCallback onPressedAddButton;
|
||||
const _Title();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -197,62 +266,9 @@ class _Title extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(name)),
|
||||
text: Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_create.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectOptionTag(
|
||||
name: name,
|
||||
fontSize: 11,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -261,13 +277,14 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
|
||||
class _SelectOptionCell extends StatefulWidget {
|
||||
const _SelectOptionCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.isSelected,
|
||||
required this.index,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final bool isSelected;
|
||||
final int index;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
@ -285,34 +302,6 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = SizedBox(
|
||||
height: 28,
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
onSelected: _onTap,
|
||||
children: [
|
||||
if (widget.isSelected)
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: _onTap,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(8, 0),
|
||||
@ -322,13 +311,59 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
mutex: widget.popoverMutex,
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.updateFocusedOption(
|
||||
widget.option.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: context
|
||||
.watch<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.focusedOptionId ==
|
||||
widget.option.id
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: null,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
index: widget.index,
|
||||
onSelected: _onTap,
|
||||
children: [
|
||||
if (context
|
||||
.watch<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.selectedOptions
|
||||
.contains(widget.option))
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: _onTap,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
@ -337,13 +372,13 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(widget.option));
|
||||
.add(SelectOptionCellEditorEvent.deleteOption(widget.option));
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
.add(SelectOptionCellEditorEvent.updateOption(updatedOption));
|
||||
},
|
||||
key: ValueKey(
|
||||
widget.option.id,
|
||||
@ -355,14 +390,149 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
|
||||
void _onTap() {
|
||||
widget.popoverMutex.close();
|
||||
if (widget.isSelected) {
|
||||
if (context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.selectedOptions
|
||||
.contains(widget.option)) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.unSelectOption(widget.option.id));
|
||||
.add(SelectOptionCellEditorEvent.unSelectOption(widget.option.id));
|
||||
} else {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectOption(widget.option.id));
|
||||
.add(SelectOptionCellEditorEvent.selectOption(widget.option.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTagCell extends StatelessWidget {
|
||||
const SelectOptionTagCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.onSelected,
|
||||
this.children = const [],
|
||||
this.index,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final VoidCallback onSelected;
|
||||
final List<Widget> children;
|
||||
final int? index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (index != null)
|
||||
ReorderableDragStartListener(
|
||||
index: index!,
|
||||
child: MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
onTap: onSelected,
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onSelected,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.suggestion,
|
||||
});
|
||||
|
||||
final CreateSelectOptionSuggestion suggestion;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 28,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
context.watch<SelectOptionCellEditorBloc>().state.focusedOptionId ==
|
||||
createSelectOptionSuggestionId
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: null,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(const SelectOptionCellEditorEvent.createOption()),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
const SelectOptionCellEditorEvent.updateFocusedOption(
|
||||
createSelectOptionSuggestionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_create.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectOptionTag(
|
||||
name: suggestion.name,
|
||||
color: suggestion.color.toColor(context),
|
||||
fontSize: 11,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities
|
||||
import 'package:flowy_infra/size.dart';
|
||||
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:flutter/services.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
|
||||
@ -18,6 +15,7 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
required this.distanceToText,
|
||||
required this.textSeparators,
|
||||
required this.textController,
|
||||
required this.focusNode,
|
||||
required this.onSubmitted,
|
||||
required this.newText,
|
||||
required this.onPaste,
|
||||
@ -30,8 +28,9 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
final double distanceToText;
|
||||
final List<String> textSeparators;
|
||||
final TextEditingController textController;
|
||||
final FocusNode focusNode;
|
||||
|
||||
final Function(String) onSubmitted;
|
||||
final Function() onSubmitted;
|
||||
final Function(String) newText;
|
||||
final Function(List<String>, String) onPaste;
|
||||
final Function(String) onRemove;
|
||||
@ -42,32 +41,11 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
late final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
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),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
widget.focusNode.requestFocus();
|
||||
});
|
||||
widget.textController.addListener(_onChanged);
|
||||
}
|
||||
@ -75,7 +53,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textController.removeListener(_onChanged);
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -83,15 +60,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: widget.textController,
|
||||
focusNode: focusNode,
|
||||
focusNode: widget.focusNode,
|
||||
onTap: widget.onClick,
|
||||
onSubmitted: (text) {
|
||||
if (text.isNotEmpty) {
|
||||
widget.onSubmitted(text.trim());
|
||||
focusNode.requestFocus();
|
||||
widget.textController.clear();
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) => widget.onSubmitted(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
@ -100,11 +71,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
),
|
||||
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),
|
||||
@ -148,23 +114,26 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
)
|
||||
.toList();
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
return Focus(
|
||||
descendantsAreFocusable: false,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/select_option_type_option_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -48,16 +50,15 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
|
||||
] else
|
||||
const _AddOptionButton(),
|
||||
const VSpace(4),
|
||||
...state.options.map((option) {
|
||||
return _OptionCell(
|
||||
option: option,
|
||||
Flexible(
|
||||
child: _OptionList(
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
@ -90,9 +91,15 @@ class _OptionTitle extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _OptionCell extends StatefulWidget {
|
||||
const _OptionCell({required this.option, this.popoverMutex});
|
||||
const _OptionCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.index,
|
||||
this.popoverMutex,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final int index;
|
||||
final PopoverMutex? popoverMutex;
|
||||
|
||||
@override
|
||||
@ -108,6 +115,7 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
height: 28,
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
index: widget.index,
|
||||
onSelected: () => _popoverController.show(),
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
@ -115,8 +123,9 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
FlowySvgs.three_dots_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -253,3 +262,61 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionList extends StatelessWidget {
|
||||
const _OptionList({
|
||||
this.popoverMutex,
|
||||
});
|
||||
|
||||
final PopoverMutex? popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
|
||||
builder: (context, state) {
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
onReorderStart: (_) => popoverMutex?.close(),
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<SelectOptionTypeOptionBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) => _OptionCell(
|
||||
key: ValueKey("select_type_option_list_${state.options[index].id}"),
|
||||
index: index,
|
||||
option: state.options[index],
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
itemCount: state.options.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromOptionId = state.options[oldIndex].id;
|
||||
final toOptionId = state.options[newIndex].id;
|
||||
context.read<SelectOptionTypeOptionBloc>().add(
|
||||
SelectOptionTypeOptionEvent.reorderOption(
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +230,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
color.optionName(),
|
||||
color.colorName(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: colorIcon,
|
||||
|
Reference in New Issue
Block a user