mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-04-25 07:07:32 -04:00
feat: using wrap to auto expand Multi-select or single select
This commit is contained in:
parent
40443ced80
commit
1ad7e0ece2
11 changed files with 160 additions and 153 deletions
|
@ -9,8 +9,8 @@ class GridSize {
|
||||||
static double get leadingHeaderPadding => 50 * scale;
|
static double get leadingHeaderPadding => 50 * scale;
|
||||||
static double get trailHeaderPadding => 140 * scale;
|
static double get trailHeaderPadding => 140 * scale;
|
||||||
static double get headerContainerPadding => 0 * scale;
|
static double get headerContainerPadding => 0 * scale;
|
||||||
static double get cellHPadding => 10 * scale;
|
static double get cellHPadding => 12 * scale;
|
||||||
static double get cellVPadding => 8 * scale;
|
static double get cellVPadding => 12 * scale;
|
||||||
static double get typeOptionItemHeight => 32 * scale;
|
static double get typeOptionItemHeight => 32 * scale;
|
||||||
static double get typeOptionSeparatorHeight => 6 * scale;
|
static double get typeOptionSeparatorHeight => 6 * scale;
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,12 @@ import 'package:app_flowy/workspace/application/grid/cell/cell_service.dart';
|
||||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
|
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart' show FieldType;
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
||||||
|
import 'package:flowy_infra/theme.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'checkbox_cell.dart';
|
import 'checkbox_cell.dart';
|
||||||
import 'date_cell.dart';
|
import 'date_cell.dart';
|
||||||
import 'number_cell.dart';
|
import 'number_cell.dart';
|
||||||
|
@ -57,3 +63,114 @@ class GridCellRequestFocusNotifier extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class GridCellStyle {}
|
abstract class GridCellStyle {}
|
||||||
|
|
||||||
|
class CellStateNotifier extends ChangeNotifier {
|
||||||
|
bool _isFocus = false;
|
||||||
|
bool _onEnter = false;
|
||||||
|
|
||||||
|
set isFocus(bool value) {
|
||||||
|
if (_isFocus != value) {
|
||||||
|
_isFocus = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set onEnter(bool value) {
|
||||||
|
if (_onEnter != value) {
|
||||||
|
_onEnter = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get isFocus => _isFocus;
|
||||||
|
|
||||||
|
bool get onEnter => _onEnter;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CellContainer extends StatelessWidget {
|
||||||
|
final GridCellWidget child;
|
||||||
|
final Widget? expander;
|
||||||
|
final double width;
|
||||||
|
final RegionStateNotifier rowStateNotifier;
|
||||||
|
const CellContainer({
|
||||||
|
Key? key,
|
||||||
|
required this.child,
|
||||||
|
required this.width,
|
||||||
|
required this.rowStateNotifier,
|
||||||
|
this.expander,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
|
||||||
|
create: (_) => CellStateNotifier(),
|
||||||
|
update: (_, row, cell) => cell!..onEnter = row.onEnter,
|
||||||
|
child: Selector<CellStateNotifier, bool>(
|
||||||
|
selector: (context, notifier) => notifier.isFocus,
|
||||||
|
builder: (context, isFocus, _) {
|
||||||
|
Widget container = Center(child: child);
|
||||||
|
child.onFocus.addListener(() {
|
||||||
|
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (expander != null) {
|
||||||
|
container = _CellEnterRegion(child: container, expander: expander!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () => child.requestFocus.notify(),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: width),
|
||||||
|
decoration: _makeBoxDecoration(context, isFocus),
|
||||||
|
padding: GridSize.cellContentInsets,
|
||||||
|
child: container,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
||||||
|
final theme = context.watch<AppTheme>();
|
||||||
|
if (isFocus) {
|
||||||
|
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
||||||
|
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
||||||
|
} else {
|
||||||
|
final borderSide = BorderSide(color: theme.shader5, width: 1.0);
|
||||||
|
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CellEnterRegion extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Widget expander;
|
||||||
|
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<CellStateNotifier, bool>(
|
||||||
|
selector: (context, notifier) => notifier.onEnter,
|
||||||
|
builder: (context, onEnter, _) {
|
||||||
|
List<Widget> children = [child];
|
||||||
|
if (onEnter) {
|
||||||
|
children.add(expander.positioned(right: 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
|
||||||
|
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
|
||||||
|
child: Stack(
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
// alignment: AlignmentDirectional.centerEnd,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,115 +0,0 @@
|
||||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/widgets/row/grid_row.dart';
|
|
||||||
import 'package:flowy_infra/theme.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
|
||||||
import 'cell_builder.dart';
|
|
||||||
|
|
||||||
class CellStateNotifier extends ChangeNotifier {
|
|
||||||
bool _isFocus = false;
|
|
||||||
bool _onEnter = false;
|
|
||||||
|
|
||||||
set isFocus(bool value) {
|
|
||||||
if (_isFocus != value) {
|
|
||||||
_isFocus = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set onEnter(bool value) {
|
|
||||||
if (_onEnter != value) {
|
|
||||||
_onEnter = value;
|
|
||||||
notifyListeners();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get isFocus => _isFocus;
|
|
||||||
|
|
||||||
bool get onEnter => _onEnter;
|
|
||||||
}
|
|
||||||
|
|
||||||
class CellContainer extends StatelessWidget {
|
|
||||||
final GridCellWidget child;
|
|
||||||
final Widget? expander;
|
|
||||||
final double width;
|
|
||||||
final RegionStateNotifier rowStateNotifier;
|
|
||||||
const CellContainer({
|
|
||||||
Key? key,
|
|
||||||
required this.child,
|
|
||||||
required this.width,
|
|
||||||
required this.rowStateNotifier,
|
|
||||||
this.expander,
|
|
||||||
}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ChangeNotifierProxyProvider<RegionStateNotifier, CellStateNotifier>(
|
|
||||||
create: (_) => CellStateNotifier(),
|
|
||||||
update: (_, row, cell) => cell!..onEnter = row.onEnter,
|
|
||||||
child: Selector<CellStateNotifier, bool>(
|
|
||||||
selector: (context, notifier) => notifier.isFocus,
|
|
||||||
builder: (context, isFocus, _) {
|
|
||||||
Widget container = Center(child: child);
|
|
||||||
child.onFocus.addListener(() {
|
|
||||||
Provider.of<CellStateNotifier>(context, listen: false).isFocus = child.onFocus.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (expander != null) {
|
|
||||||
container = _CellEnterRegion(child: container, expander: expander!);
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
behavior: HitTestBehavior.translucent,
|
|
||||||
onTap: () => child.requestFocus.notify(),
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: width),
|
|
||||||
decoration: _makeBoxDecoration(context, isFocus),
|
|
||||||
padding: GridSize.cellContentInsets,
|
|
||||||
child: container,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
BoxDecoration _makeBoxDecoration(BuildContext context, bool isFocus) {
|
|
||||||
final theme = context.watch<AppTheme>();
|
|
||||||
if (isFocus) {
|
|
||||||
final borderSide = BorderSide(color: theme.main1, width: 1.0);
|
|
||||||
return BoxDecoration(border: Border.fromBorderSide(borderSide));
|
|
||||||
} else {
|
|
||||||
final borderSide = BorderSide(color: theme.shader4, width: 0.4);
|
|
||||||
return BoxDecoration(border: Border(right: borderSide, bottom: borderSide));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CellEnterRegion extends StatelessWidget {
|
|
||||||
final Widget child;
|
|
||||||
final Widget expander;
|
|
||||||
const _CellEnterRegion({required this.child, required this.expander, Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Selector<CellStateNotifier, bool>(
|
|
||||||
selector: (context, notifier) => notifier.onEnter,
|
|
||||||
builder: (context, onEnter, _) {
|
|
||||||
List<Widget> children = [Expanded(child: child)];
|
|
||||||
if (onEnter) {
|
|
||||||
children.add(expander);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
onEnter: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = true,
|
|
||||||
onExit: (p) => Provider.of<CellStateNotifier>(context, listen: false).onEnter = false,
|
|
||||||
child: Row(
|
|
||||||
// alignment: AlignmentDirectional.centerEnd,
|
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,7 +36,7 @@ class _CheckboxCellState extends State<CheckboxCell> {
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck');
|
final icon = state.isSelected ? svgWidget('editor/editor_check') : svgWidget('editor/editor_uncheck');
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 42,
|
height: 20,
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.centerLeft,
|
||||||
child: FlowyIconButton(
|
child: FlowyIconButton(
|
||||||
|
|
|
@ -55,7 +55,7 @@ class _NumberCellState extends State<NumberCell> {
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onEditingComplete: () => _focusNode.unfocus(),
|
onEditingComplete: () => _focusNode.unfocus(),
|
||||||
maxLines: 1,
|
maxLines: null,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
export 'cell_builder.dart';
|
export 'cell_builder.dart';
|
||||||
export 'cell_container.dart';
|
|
||||||
export 'text_cell.dart';
|
export 'text_cell.dart';
|
||||||
export 'number_cell.dart';
|
export 'number_cell.dart';
|
||||||
export 'date_cell.dart';
|
export 'date_cell.dart';
|
||||||
|
|
|
@ -66,15 +66,25 @@ class SelectOptionTag extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return ChoiceChip(
|
||||||
decoration: BoxDecoration(
|
pressElevation: 1,
|
||||||
color: option.color.make(context),
|
label: FlowyText.medium(option.name, fontSize: 12),
|
||||||
shape: BoxShape.rectangle,
|
selectedColor: option.color.make(context),
|
||||||
borderRadius: BorderRadius.circular(8.0),
|
backgroundColor: option.color.make(context),
|
||||||
),
|
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
|
selected: true,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 3.0),
|
onSelected: (_) {},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// return Container(
|
||||||
|
// decoration: BoxDecoration(
|
||||||
|
// color: option.color.make(context),
|
||||||
|
// shape: BoxShape.rectangle,
|
||||||
|
// borderRadius: BorderRadius.circular(8.0),
|
||||||
|
// ),
|
||||||
|
// child: Center(child: FlowyText.medium(option.name, fontSize: 12)),
|
||||||
|
// margin: const EdgeInsets.symmetric(horizontal: 3.0),
|
||||||
|
// padding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
|
// );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
// Log.trace("init widget $hashCode");
|
|
||||||
final cellContext = _buildCellContext();
|
final cellContext = _buildCellContext();
|
||||||
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
_cellBloc = getIt<SelectionCellBloc>(param1: cellContext)..add(const SelectionCellEvent.initial());
|
||||||
super.initState();
|
super.initState();
|
||||||
|
@ -64,8 +63,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||||
if (children.isEmpty && widget.cellStyle != null) {
|
if (children.isEmpty && widget.cellStyle != null) {
|
||||||
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3));
|
children.add(FlowyText.medium(widget.cellStyle!.placeholder, fontSize: 14, color: theme.shader3));
|
||||||
}
|
}
|
||||||
return SizedBox(
|
return SizedBox.expand(
|
||||||
height: 69,
|
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
widget.onFocus.value = true;
|
widget.onFocus.value = true;
|
||||||
|
@ -75,7 +73,7 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||||
() => widget.onFocus.value = false,
|
() => widget.onFocus.value = false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ClipRRect(child: Row(children: children)),
|
child: Center(child: Wrap(children: children)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -89,7 +87,6 @@ class _SingleSelectCellState extends State<SingleSelectCell> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
// Log.trace("dispose widget $hashCode");
|
|
||||||
_cellBloc.close();
|
_cellBloc.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
@ -148,7 +145,7 @@ class _MultiSelectCellState extends State<MultiSelectCell> {
|
||||||
() => widget.onFocus.value = false,
|
() => widget.onFocus.value = false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: ClipRRect(child: Row(children: children)),
|
child: Wrap(children: children, spacing: 4, runSpacing: 4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -94,7 +94,7 @@ class SelectOptionTextField extends StatelessWidget {
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
controller: sc,
|
controller: sc,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
child: Row(children: children),
|
child: Wrap(children: children, spacing: 4),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,14 +68,12 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||||
},
|
},
|
||||||
buildWhen: (previous, current) => previous.content != current.content,
|
buildWhen: (previous, current) => previous.content != current.content,
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return SizedBox(
|
return TextField(
|
||||||
height: 42,
|
|
||||||
child: TextField(
|
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onChanged: (value) => focusChanged(),
|
onChanged: (value) => focusChanged(),
|
||||||
onEditingComplete: () => _focusNode.unfocus(),
|
onEditingComplete: () => _focusNode.unfocus(),
|
||||||
maxLines: 1,
|
maxLines: null,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
|
@ -83,7 +81,6 @@ class _GridTextCellState extends State<GridTextCell> {
|
||||||
hintText: widget.cellStyle?.placeholder,
|
hintText: widget.cellStyle?.placeholder,
|
||||||
isDense: true,
|
isDense: true,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -206,9 +206,11 @@ class _CellExpander extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = context.watch<AppTheme>();
|
final theme = context.watch<AppTheme>();
|
||||||
return FlowyIconButton(
|
return FlowyIconButton(
|
||||||
width: 20,
|
width: 30,
|
||||||
|
height: 24,
|
||||||
onPressed: onExpand,
|
onPressed: onExpand,
|
||||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||||
|
fillColor: theme.surface,
|
||||||
icon: svgWidget("grid/expander", color: theme.main1),
|
icon: svgWidget("grid/expander", color: theme.main1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue