mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-05 03:23:12 +00:00
feat: support column and row reordering in table (#6912)
* chore: update changelog * feat: add draggable in table reorder button * feat: support displaying text color, background color and font item in table cell * feat: separate gestures for popup menu and drag operations * feat: support feedback mode for table * feat: build dummy node to render table feedback * feat: disable column resize handle when dragging column * feat: higtlight the cell border when dragging * fix: unable to reorder in row * fix: do not rebuild the reorder button when reordering * feat: add reorder logic and tests * feat: reorder column * feat: reorder row * test: reorder row * fix: table attributes are broken after reordering * chore: remove unused listerner * chore: code refactor * fix: remove unused code * feat: support rendering table feedback * fix: unit test
This commit is contained in:
parent
dddf5aa195
commit
67fe0d6bfd
@ -109,18 +109,12 @@ class TextRobot {
|
||||
Position(path: next),
|
||||
);
|
||||
await editorState.apply(transaction);
|
||||
debugPrint(
|
||||
'AI insertNewParagraph: path: ${editorState.selection!.end.path}, index: ${editorState.selection!.endIndex}',
|
||||
);
|
||||
await Future.delayed(const Duration(milliseconds: 10));
|
||||
}
|
||||
|
||||
Future<void> insertText(String text, Duration delay) async {
|
||||
final selection = editorState.selection;
|
||||
debugPrint(
|
||||
'AI insertText: get selection, path: ${selection!.end.path}, index: ${selection.endIndex}',
|
||||
);
|
||||
if (!selection.isCollapsed) {
|
||||
if (selection == null || !selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.end.path);
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
@ -14,47 +12,6 @@ import 'package:provider/provider.dart';
|
||||
/// when hovering on the last row / last column / last cell.
|
||||
bool _enableHoveringLogicV2 = true;
|
||||
|
||||
class SimpleTableReorderButton extends StatelessWidget {
|
||||
const SimpleTableReorderButton({
|
||||
super.key,
|
||||
required this.isShowingMenu,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final ValueNotifier<bool> isShowingMenu;
|
||||
final SimpleTableMoreActionType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isShowingMenu,
|
||||
builder: (context, isShowingMenu, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isShowingMenu
|
||||
? context.simpleTableMoreActionHoverColor
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: context.simpleTableMoreActionBorderColor,
|
||||
),
|
||||
),
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
child: FlowySvg(
|
||||
type.reorderIconSvg,
|
||||
color: isShowingMenu ? Colors.white : null,
|
||||
size: const Size.square(16.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableAddRowHoverButton extends StatefulWidget {
|
||||
const SimpleTableAddRowHoverButton({
|
||||
super.key,
|
||||
@ -549,96 +506,6 @@ class SimpleTableBasicButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableColumnResizeHandle extends StatefulWidget {
|
||||
const SimpleTableColumnResizeHandle({
|
||||
super.key,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
State<SimpleTableColumnResizeHandle> createState() =>
|
||||
_SimpleTableColumnResizeHandleState();
|
||||
}
|
||||
|
||||
class _SimpleTableColumnResizeHandleState
|
||||
extends State<SimpleTableColumnResizeHandle> {
|
||||
bool isStartDragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeColumn,
|
||||
onEnter: (event) => context
|
||||
.read<SimpleTableContext>()
|
||||
.hoveringOnResizeHandle
|
||||
.value = widget.node,
|
||||
onExit: (event) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
// the onExit event will be triggered before dragging started.
|
||||
// delay the hiding of the resize handle to avoid flickering.
|
||||
if (!isStartDragging) {
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle.value =
|
||||
null;
|
||||
}
|
||||
});
|
||||
},
|
||||
child: GestureDetector(
|
||||
onHorizontalDragStart: (details) {
|
||||
// disable the two-finger drag on trackpad
|
||||
if (details.kind == PointerDeviceKind.trackpad) {
|
||||
return;
|
||||
}
|
||||
isStartDragging = true;
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
if (!isStartDragging) {
|
||||
return;
|
||||
}
|
||||
context.read<EditorState>().updateColumnWidthInMemory(
|
||||
tableCellNode: widget.node,
|
||||
deltaX: details.delta.dx,
|
||||
);
|
||||
},
|
||||
onHorizontalDragEnd: (details) {
|
||||
if (!isStartDragging) {
|
||||
return;
|
||||
}
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle.value =
|
||||
null;
|
||||
isStartDragging = false;
|
||||
context.read<EditorState>().updateColumnWidth(
|
||||
tableCellNode: widget.node,
|
||||
width: widget.node.columnWidth,
|
||||
);
|
||||
},
|
||||
child: ValueListenableBuilder<Node?>(
|
||||
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
|
||||
builder: (context, hoveringCell, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle,
|
||||
builder: (context, hoveringOnResizeHandle, child) {
|
||||
final isSameRowIndex = hoveringOnResizeHandle?.columnIndex ==
|
||||
widget.node.columnIndex;
|
||||
return Opacity(
|
||||
opacity: isSameRowIndex ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: SimpleTableConstants.resizeHandleWidth,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableBackgroundColorMenu extends StatefulWidget {
|
||||
const SimpleTableBackgroundColorMenu({
|
||||
super.key,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -187,16 +187,21 @@ class _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = Transform.translate(
|
||||
offset: Offset(
|
||||
UniversalPlatform.isDesktop
|
||||
? -SimpleTableConstants.tableLeftPadding
|
||||
: 0,
|
||||
0,
|
||||
),
|
||||
child: _buildTable(),
|
||||
Widget child = SimpleTableWidget(
|
||||
node: node,
|
||||
simpleTableContext: simpleTableContext,
|
||||
);
|
||||
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
child = Transform.translate(
|
||||
offset: const Offset(
|
||||
-SimpleTableConstants.tableLeftPadding,
|
||||
0,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
child = Container(
|
||||
alignment: Alignment.topLeft,
|
||||
padding: padding,
|
||||
@ -228,111 +233,6 @@ class _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildTable() {
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
return _buildDesktopTable();
|
||||
} else {
|
||||
return _buildMobileTable();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDesktopTable() {
|
||||
// IntrinsicWidth and IntrinsicHeight are used to make the table size fit the content.
|
||||
return MouseRegion(
|
||||
onEnter: (event) => simpleTableContext.isHoveringOnTableArea.value = true,
|
||||
onExit: (event) {
|
||||
simpleTableContext.isHoveringOnTableArea.value = false;
|
||||
},
|
||||
child: Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: Stack(
|
||||
children: [
|
||||
MouseRegion(
|
||||
hitTestBehavior: HitTestBehavior.opaque,
|
||||
onEnter: (event) =>
|
||||
simpleTableContext.isHoveringOnColumnsAndRows.value = true,
|
||||
onExit: (event) {
|
||||
simpleTableContext.isHoveringOnColumnsAndRows.value = false;
|
||||
simpleTableContext.hoveringTableCell.value = null;
|
||||
},
|
||||
child: Scrollbar(
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: SimpleTableConstants.tablePadding,
|
||||
child: IntrinsicWidth(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildRows(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (UniversalPlatform.isDesktop) ...[
|
||||
SimpleTableAddColumnHoverButton(
|
||||
editorState: editorState,
|
||||
node: node,
|
||||
),
|
||||
SimpleTableAddRowHoverButton(
|
||||
editorState: editorState,
|
||||
tableNode: node,
|
||||
),
|
||||
SimpleTableAddColumnAndRowHoverButton(
|
||||
editorState: editorState,
|
||||
node: node,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMobileTable() {
|
||||
return Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: IntrinsicWidth(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildRows(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRows() {
|
||||
final List<Widget> rows = [];
|
||||
|
||||
if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) {
|
||||
rows.add(const SimpleTableColumnDivider());
|
||||
}
|
||||
|
||||
for (final child in node.children) {
|
||||
rows.add(editorState.renderer.build(context, child));
|
||||
|
||||
if (SimpleTableConstants.borderType ==
|
||||
SimpleTableBorderRenderType.table) {
|
||||
rows.add(const SimpleTableColumnDivider());
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
void _onSelectionChanged() {
|
||||
final selection = editorState.selectionNotifier.value;
|
||||
final selectionType = editorState.selectionType;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_border_builder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_column_resize_handle.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -79,6 +80,11 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
|
||||
late SimpleTableContext? simpleTableContext =
|
||||
context.read<SimpleTableContext?>();
|
||||
late final borderBuilder = SimpleTableBorderBuilder(
|
||||
context: context,
|
||||
simpleTableContext: simpleTableContext!,
|
||||
node: node,
|
||||
);
|
||||
|
||||
ValueNotifier<bool> isEditingCellNotifier = ValueNotifier(false);
|
||||
|
||||
@ -120,17 +126,19 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_buildCell(),
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: -SimpleTableConstants.tableLeftPadding,
|
||||
child: _buildRowMoreActionButton(),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildColumnMoreActionButton(),
|
||||
),
|
||||
if (node.columnIndex == 0)
|
||||
Positioned(
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: -SimpleTableConstants.tableLeftPadding,
|
||||
child: _buildRowMoreActionButton(),
|
||||
),
|
||||
if (node.rowIndex == 0)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildColumnMoreActionButton(),
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: node.rowIndex == 0 ? SimpleTableConstants.tableTopPadding : 0,
|
||||
@ -156,27 +164,34 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
padding: EdgeInsets.only(
|
||||
top: node.rowIndex == 0 ? SimpleTableConstants.tableTopPadding : 0,
|
||||
),
|
||||
// TODO(Lucas): find a better way to handle the multiple value listenable builder
|
||||
// There's flutter pub can do that.
|
||||
child: ValueListenableBuilder<bool>(
|
||||
valueListenable: isEditingCellNotifier,
|
||||
builder: (context, isEditingCell, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: simpleTableContext!.selectingColumn,
|
||||
builder: (context, selectingColumn, child) {
|
||||
builder: (context, selectingColumn, _) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: simpleTableContext!.selectingRow,
|
||||
builder: (context, selectingRow, _) {
|
||||
return DecoratedBox(
|
||||
decoration: _buildDecoration(),
|
||||
child: child!,
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: simpleTableContext!.hoveringTableCell,
|
||||
builder: (context, hoveringTableCell, _) {
|
||||
return DecoratedBox(
|
||||
decoration: _buildDecoration(),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: node.children.map(_buildCellContent).toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: node.children.map(_buildCellContent).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -199,13 +214,8 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
}
|
||||
|
||||
Widget _buildRowMoreActionButton() {
|
||||
final columnIndex = node.columnIndex;
|
||||
final rowIndex = node.rowIndex;
|
||||
|
||||
if (columnIndex != 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SimpleTableMoreActionMenu(
|
||||
index: rowIndex,
|
||||
type: SimpleTableMoreActionType.row,
|
||||
@ -214,11 +224,6 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
|
||||
Widget _buildColumnMoreActionButton() {
|
||||
final columnIndex = node.columnIndex;
|
||||
final rowIndex = node.rowIndex;
|
||||
|
||||
if (rowIndex != 0) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SimpleTableMoreActionMenu(
|
||||
index: columnIndex,
|
||||
@ -238,7 +243,9 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
|
||||
Decoration _buildDecoration() {
|
||||
final backgroundColor = _buildBackgroundColor();
|
||||
final border = _buildBorder();
|
||||
final border = borderBuilder.buildBorder(
|
||||
isEditingCell: isEditingCellNotifier.value,
|
||||
);
|
||||
|
||||
return BoxDecoration(
|
||||
border: border,
|
||||
@ -269,29 +276,6 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
return Theme.of(context).colorScheme.surface;
|
||||
}
|
||||
|
||||
Border? _buildBorder() {
|
||||
if (SimpleTableConstants.borderType != SimpleTableBorderRenderType.cell) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final tableContext = context.watch<SimpleTableContext>();
|
||||
final isCellInSelectedColumn =
|
||||
node.columnIndex == tableContext.selectingColumn.value;
|
||||
final isCellInSelectedRow =
|
||||
node.rowIndex == tableContext.selectingRow.value;
|
||||
if (tableContext.isSelectingTable.value) {
|
||||
return _buildSelectingTableBorder();
|
||||
} else if (isCellInSelectedColumn) {
|
||||
return _buildColumnBorder();
|
||||
} else if (isCellInSelectedRow) {
|
||||
return _buildRowBorder();
|
||||
} else if (isEditingCellNotifier.value) {
|
||||
return _buildEditingBorder();
|
||||
} else {
|
||||
return _buildCellBorder();
|
||||
}
|
||||
}
|
||||
|
||||
bool _isInHeader() {
|
||||
final isHeaderColumnEnabled = node.isHeaderColumnEnabled;
|
||||
final isHeaderRowEnabled = node.isHeaderRowEnabled;
|
||||
@ -303,111 +287,6 @@ class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
|
||||
isHeaderRowEnabled && isFirstColumn;
|
||||
}
|
||||
|
||||
/// the column border means the `VERTICAL` border of the cell
|
||||
///
|
||||
/// ____
|
||||
/// | 1 | 2 |
|
||||
/// | 3 | 4 |
|
||||
/// |___|
|
||||
///
|
||||
/// the border wrapping the cell 2 and cell 4 is the column border
|
||||
Border _buildColumnBorder() {
|
||||
return Border(
|
||||
left: _buildHighlightBorderSide(),
|
||||
right: _buildHighlightBorderSide(),
|
||||
top: node.rowIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
/// the row border means the `HORIZONTAL` border of the cell
|
||||
///
|
||||
/// ________
|
||||
/// | 1 | 2 |
|
||||
/// |_______|
|
||||
/// | 3 | 4 |
|
||||
///
|
||||
/// the border wrapping the cell 1 and cell 2 is the row border
|
||||
Border _buildRowBorder() {
|
||||
return Border(
|
||||
top: _buildHighlightBorderSide(),
|
||||
bottom: _buildHighlightBorderSide(),
|
||||
left: node.columnIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
right: node.columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
Border _buildCellBorder() {
|
||||
return Border(
|
||||
top: node.rowIndex == 0
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
left: node.columnIndex == 0
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
right: node.columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
Border _buildEditingBorder() {
|
||||
return Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
);
|
||||
}
|
||||
|
||||
Border _buildSelectingTableBorder() {
|
||||
final rowIndex = node.rowIndex;
|
||||
final columnIndex = node.columnIndex;
|
||||
|
||||
return Border(
|
||||
top:
|
||||
rowIndex == 0 ? _buildHighlightBorderSide() : _buildLightBorderSide(),
|
||||
bottom: rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
left: columnIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
right: columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildHighlightBorderSide() {
|
||||
return BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildLightBorderSide() {
|
||||
return BorderSide(
|
||||
color: context.simpleTableBorderColor,
|
||||
width: 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildDefaultBorderSide() {
|
||||
return BorderSide(
|
||||
color: context.simpleTableBorderColor,
|
||||
);
|
||||
}
|
||||
|
||||
void _onSelectingTableChanged() {
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
|
||||
@ -5,11 +5,11 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
const enableTableDebugLog = false;
|
||||
const _enableTableDebugLog = false;
|
||||
|
||||
class SimpleTableContext {
|
||||
SimpleTableContext() {
|
||||
if (enableTableDebugLog) {
|
||||
if (_enableTableDebugLog) {
|
||||
isHoveringOnColumnsAndRows.addListener(
|
||||
_onHoveringOnColumnsAndRowsChanged,
|
||||
);
|
||||
@ -21,39 +21,56 @@ class SimpleTableContext {
|
||||
selectingRow.addListener(_onSelectingRowChanged);
|
||||
isSelectingTable.addListener(_onSelectingTableChanged);
|
||||
isHoveringOnTableBlock.addListener(_onHoveringOnTableBlockChanged);
|
||||
isReorderingColumn.addListener(_onDraggingColumnChanged);
|
||||
isReorderingRow.addListener(_onDraggingRowChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// the area only contains the columns and rows,
|
||||
// the add row button, add column button, and add column and row button are not part of the table area
|
||||
/// the area only contains the columns and rows,
|
||||
/// the add row button, add column button, and add column and row button are not part of the table area
|
||||
final ValueNotifier<bool> isHoveringOnColumnsAndRows = ValueNotifier(false);
|
||||
|
||||
// the table area contains the columns and rows,
|
||||
// the add row button, add column button, and add column and row button are not part of the table area,
|
||||
// not including the selection area and padding
|
||||
/// the table area contains the columns and rows,
|
||||
/// the add row button, add column button, and add column and row button are not part of the table area,
|
||||
/// not including the selection area and padding
|
||||
final ValueNotifier<bool> isHoveringOnTableArea = ValueNotifier(false);
|
||||
|
||||
// the table block area contains the table area and the add row button, add column button, and add column and row button
|
||||
// also, the table block area contains the selection area and padding
|
||||
/// the table block area contains the table area and the add row button, add column button, and add column and row button
|
||||
/// also, the table block area contains the selection area and padding
|
||||
final ValueNotifier<bool> isHoveringOnTableBlock = ValueNotifier(false);
|
||||
|
||||
// the hovering table cell is the cell that the mouse is hovering on
|
||||
/// the hovering table cell is the cell that the mouse is hovering on
|
||||
final ValueNotifier<Node?> hoveringTableCell = ValueNotifier(null);
|
||||
|
||||
// the hovering on resize handle is the resize handle that the mouse is hovering on
|
||||
/// the hovering on resize handle is the resize handle that the mouse is hovering on
|
||||
final ValueNotifier<Node?> hoveringOnResizeHandle = ValueNotifier(null);
|
||||
|
||||
// the selecting column is the column that the user is selecting
|
||||
/// the selecting column is the column that the user is selecting
|
||||
final ValueNotifier<int?> selectingColumn = ValueNotifier(null);
|
||||
|
||||
// the selecting row is the row that the user is selecting
|
||||
/// the selecting row is the row that the user is selecting
|
||||
final ValueNotifier<int?> selectingRow = ValueNotifier(null);
|
||||
|
||||
// the is selecting table is the table that the user is selecting
|
||||
/// the is selecting table is the table that the user is selecting
|
||||
final ValueNotifier<bool> isSelectingTable = ValueNotifier(false);
|
||||
|
||||
/// isReorderingColumn is a tuple of (isReordering, columnIndex)
|
||||
final ValueNotifier<(bool, int)> isReorderingColumn =
|
||||
ValueNotifier((false, -1));
|
||||
|
||||
/// isReorderingRow is a tuple of (isReordering, rowIndex)
|
||||
final ValueNotifier<(bool, int)> isReorderingRow = ValueNotifier((false, -1));
|
||||
|
||||
/// reorderingOffset is the offset of the reordering
|
||||
//
|
||||
/// This value is only available when isReordering is true
|
||||
final ValueNotifier<Offset> reorderingOffset = ValueNotifier(Offset.zero);
|
||||
|
||||
bool get isReordering =>
|
||||
isReorderingColumn.value.$1 || isReorderingRow.value.$1;
|
||||
|
||||
void _onHoveringOnColumnsAndRowsChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -61,7 +78,7 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onHoveringTableNodeChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -74,7 +91,7 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onSelectingColumnChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -82,7 +99,7 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onSelectingRowChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -90,7 +107,7 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onSelectingTableChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -98,7 +115,7 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onHoveringOnTableBlockChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -106,13 +123,29 @@ class SimpleTableContext {
|
||||
}
|
||||
|
||||
void _onHoveringOnTableAreaChanged() {
|
||||
if (!enableTableDebugLog) {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug('isHoveringOnTableArea: ${isHoveringOnTableArea.value}');
|
||||
}
|
||||
|
||||
void _onDraggingColumnChanged() {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug('isDraggingColumn: ${isReorderingColumn.value}');
|
||||
}
|
||||
|
||||
void _onDraggingRowChanged() {
|
||||
if (!_enableTableDebugLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.debug('isDraggingRow: ${isReorderingRow.value}');
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
isHoveringOnColumnsAndRows.dispose();
|
||||
isHoveringOnTableBlock.dispose();
|
||||
@ -122,11 +155,14 @@ class SimpleTableContext {
|
||||
selectingColumn.dispose();
|
||||
selectingRow.dispose();
|
||||
isSelectingTable.dispose();
|
||||
isReorderingColumn.dispose();
|
||||
isReorderingRow.dispose();
|
||||
reorderingOffset.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableConstants {
|
||||
// Table
|
||||
/// Table
|
||||
static const defaultColumnWidth = 120.0;
|
||||
static const minimumColumnWidth = 36.0;
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_tab
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_reorder_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -50,6 +51,14 @@ enum SimpleTableMoreActionType {
|
||||
return FlowySvgs.table_reorder_row_s;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return switch (this) {
|
||||
SimpleTableMoreActionType.column => 'column',
|
||||
SimpleTableMoreActionType.row => 'row',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum SimpleTableMoreAction {
|
||||
@ -143,6 +152,7 @@ class _SimpleTableMoreActionMenuState extends State<SimpleTableMoreActionMenu> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
return Align(
|
||||
alignment: widget.type == SimpleTableMoreActionType.row
|
||||
? Alignment.centerLeft
|
||||
@ -151,9 +161,24 @@ class _SimpleTableMoreActionMenuState extends State<SimpleTableMoreActionMenu> {
|
||||
valueListenable: isShowingMenu,
|
||||
builder: (context, isShowingMenu, child) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable:
|
||||
context.read<SimpleTableContext>().hoveringTableCell,
|
||||
valueListenable: simpleTableContext.hoveringTableCell,
|
||||
builder: (context, hoveringTableNode, child) {
|
||||
final reorderingIndex = switch (widget.type) {
|
||||
SimpleTableMoreActionType.column =>
|
||||
simpleTableContext.isReorderingColumn.value.$2,
|
||||
SimpleTableMoreActionType.row =>
|
||||
simpleTableContext.isReorderingRow.value.$2,
|
||||
};
|
||||
final isReordering = simpleTableContext.isReordering;
|
||||
if (isReordering) {
|
||||
// when reordering, hide the menu for another column or row that is not the current dragging one.
|
||||
if (reorderingIndex != widget.index) {
|
||||
return const SizedBox.shrink();
|
||||
} else {
|
||||
return child!;
|
||||
}
|
||||
}
|
||||
|
||||
final hoveringIndex =
|
||||
widget.type == SimpleTableMoreActionType.column
|
||||
? hoveringTableNode?.columnIndex
|
||||
@ -166,7 +191,6 @@ class _SimpleTableMoreActionMenuState extends State<SimpleTableMoreActionMenu> {
|
||||
return child!;
|
||||
},
|
||||
child: SimpleTableMoreActionPopup(
|
||||
key: ValueKey(widget.type.name + widget.index.toString()),
|
||||
index: widget.index,
|
||||
isShowingMenu: this.isShowingMenu,
|
||||
type: widget.type,
|
||||
@ -198,6 +222,7 @@ class SimpleTableMoreActionPopup extends StatefulWidget {
|
||||
class _SimpleTableMoreActionPopupState
|
||||
extends State<SimpleTableMoreActionPopup> {
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
SelectionGestureInterceptor? gestureInterceptor;
|
||||
|
||||
RenderBox? get renderBox => context.findRenderObject() as RenderBox?;
|
||||
@ -230,67 +255,85 @@ class _SimpleTableMoreActionPopupState
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tableCellNode =
|
||||
context.read<SimpleTableContext>().hoveringTableCell.value;
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
final tableCellNode = simpleTableContext.hoveringTableCell.value;
|
||||
final tableNode = tableCellNode?.parentTableNode;
|
||||
|
||||
if (tableNode == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return AppFlowyPopover(
|
||||
onOpen: () {
|
||||
widget.isShowingMenu.value = true;
|
||||
switch (widget.type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
context.read<SimpleTableContext>().selectingColumn.value =
|
||||
tableCellNode?.columnIndex;
|
||||
case SimpleTableMoreActionType.row:
|
||||
context.read<SimpleTableContext>().selectingRow.value =
|
||||
tableCellNode?.rowIndex;
|
||||
}
|
||||
|
||||
// Workaround to clear the selection after the menu is opened.
|
||||
Future.delayed(Durations.short3, () {
|
||||
if (!editorState.isDisposed) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
onClose: () {
|
||||
widget.isShowingMenu.value = false;
|
||||
|
||||
// clear the selecting index
|
||||
context.read<SimpleTableContext>().selectingColumn.value = null;
|
||||
context.read<SimpleTableContext>().selectingRow.value = null;
|
||||
},
|
||||
onOpen: () => _onOpen(tableCellNode: tableCellNode),
|
||||
onClose: () => _onClose(),
|
||||
direction: widget.type == SimpleTableMoreActionType.row
|
||||
? PopoverDirection.bottomWithCenterAligned
|
||||
: PopoverDirection.bottomWithLeftAligned,
|
||||
offset: widget.type == SimpleTableMoreActionType.row
|
||||
? const Offset(24, 14)
|
||||
: const Offset(-14, 8),
|
||||
popupBuilder: (_) {
|
||||
if (tableCellNode == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(
|
||||
value: context.read<SimpleTableContext>(),
|
||||
),
|
||||
Provider.value(
|
||||
value: context.read<EditorState>(),
|
||||
),
|
||||
],
|
||||
child: SimpleTableMoreActionList(
|
||||
type: widget.type,
|
||||
index: widget.index,
|
||||
tableCellNode: tableCellNode,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: SimpleTableReorderButton(
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
popupBuilder: (_) => _buildPopup(tableCellNode: tableCellNode),
|
||||
child: SimpleTableDraggableReorderButton(
|
||||
editorState: editorState,
|
||||
simpleTableContext: simpleTableContext,
|
||||
node: tableNode,
|
||||
index: widget.index,
|
||||
isShowingMenu: widget.isShowingMenu,
|
||||
type: widget.type,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPopup({Node? tableCellNode}) {
|
||||
if (tableCellNode == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider.value(
|
||||
value: context.read<SimpleTableContext>(),
|
||||
),
|
||||
Provider.value(
|
||||
value: context.read<EditorState>(),
|
||||
),
|
||||
],
|
||||
child: SimpleTableMoreActionList(
|
||||
type: widget.type,
|
||||
index: widget.index,
|
||||
tableCellNode: tableCellNode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onOpen({Node? tableCellNode}) {
|
||||
widget.isShowingMenu.value = true;
|
||||
|
||||
switch (widget.type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
context.read<SimpleTableContext>().selectingColumn.value =
|
||||
tableCellNode?.columnIndex;
|
||||
case SimpleTableMoreActionType.row:
|
||||
context.read<SimpleTableContext>().selectingRow.value =
|
||||
tableCellNode?.rowIndex;
|
||||
}
|
||||
|
||||
// Workaround to clear the selection after the menu is opened.
|
||||
Future.delayed(Durations.short3, () {
|
||||
if (!editorState.isDisposed) {
|
||||
editorState.selection = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onClose() {
|
||||
widget.isShowingMenu.value = false;
|
||||
|
||||
// clear the selecting index
|
||||
context.read<SimpleTableContext>().selectingColumn.value = null;
|
||||
context.read<SimpleTableContext>().selectingRow.value = null;
|
||||
}
|
||||
|
||||
bool _isTapInBounds(Offset offset) {
|
||||
if (renderBox == null) {
|
||||
return false;
|
||||
@ -506,22 +549,16 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
_deleteRow();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case SimpleTableMoreAction.insertLeft:
|
||||
_insertColumnLeft();
|
||||
break;
|
||||
case SimpleTableMoreAction.insertRight:
|
||||
_insertColumnRight();
|
||||
break;
|
||||
case SimpleTableMoreAction.insertAbove:
|
||||
_insertRowAbove();
|
||||
break;
|
||||
case SimpleTableMoreAction.insertBelow:
|
||||
_insertRowBelow();
|
||||
break;
|
||||
case SimpleTableMoreAction.clearContents:
|
||||
_clearContent();
|
||||
break;
|
||||
case SimpleTableMoreAction.duplicate:
|
||||
switch (widget.type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
@ -531,7 +568,6 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
_duplicateRow();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@ -581,6 +617,8 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
|
||||
isEnableHeader.value,
|
||||
);
|
||||
}
|
||||
|
||||
PopoverContainer.of(context).close();
|
||||
}
|
||||
|
||||
void _clearContent() {
|
||||
|
||||
@ -10,6 +10,8 @@ enum TableMapOperationType {
|
||||
deleteColumn,
|
||||
duplicateRow,
|
||||
duplicateColumn,
|
||||
reorderColumn,
|
||||
reorderRow,
|
||||
}
|
||||
|
||||
extension TableMapOperation on Node {
|
||||
@ -17,6 +19,8 @@ extension TableMapOperation on Node {
|
||||
Node node, {
|
||||
required TableMapOperationType type,
|
||||
required int index,
|
||||
// Only used for reorder column operation
|
||||
int? toIndex,
|
||||
}) {
|
||||
assert(this.type == SimpleTableBlockKeys.type);
|
||||
|
||||
@ -39,10 +43,20 @@ extension TableMapOperation on Node {
|
||||
attributes = _mapRowDeletionAttributes(index);
|
||||
case TableMapOperationType.deleteColumn:
|
||||
attributes = _mapColumnDeletionAttributes(index);
|
||||
case TableMapOperationType.reorderColumn:
|
||||
if (toIndex != null) {
|
||||
attributes = _mapColumnReorderingAttributes(index, toIndex);
|
||||
}
|
||||
case TableMapOperationType.reorderRow:
|
||||
if (toIndex != null) {
|
||||
attributes = _mapRowReorderingAttributes(index, toIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// clear the attributes that are null
|
||||
attributes?.removeWhere((key, value) => value == null);
|
||||
attributes?.removeWhere(
|
||||
(key, value) => value == null,
|
||||
);
|
||||
|
||||
return attributes;
|
||||
}
|
||||
@ -410,6 +424,7 @@ extension TableMapOperation on Node {
|
||||
comparator: (iKey, index) => iKey > index,
|
||||
filterIndex: index,
|
||||
);
|
||||
|
||||
final rowAligns = _remapSource(
|
||||
this.rowAligns,
|
||||
index,
|
||||
@ -432,6 +447,319 @@ extension TableMapOperation on Node {
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the attributes of a column reordering operation.
|
||||
///
|
||||
///
|
||||
/// Examples:
|
||||
/// Case 1:
|
||||
///
|
||||
/// When reordering a column, if the from index is greater than the to index,
|
||||
/// the attributes of the table before the from index should be updated.
|
||||
///
|
||||
/// Before:
|
||||
/// ↓ reorder this column from index 1 to index 0
|
||||
/// | 0 | 1 | 2 |
|
||||
/// | 3 | 4 | 5 |
|
||||
///
|
||||
/// The original attributes of the table:
|
||||
/// {
|
||||
/// "rowColors": {
|
||||
/// 0: "#FF0000",
|
||||
/// 1: "#00FF00",
|
||||
/// 2: "#0000FF",
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// After reordering:
|
||||
/// | 1 | 0 | 2 |
|
||||
/// | 4 | 3 | 5 |
|
||||
///
|
||||
/// The new attributes of the table:
|
||||
/// {
|
||||
/// "rowColors": {
|
||||
/// 0: "#00FF00", ← The attributes of the original second column
|
||||
/// 1: "#FF0000", ← The attributes of the original first column
|
||||
/// 2: "#0000FF",
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Case 2:
|
||||
///
|
||||
/// When reordering a column, if the from index is less than the to index,
|
||||
/// the attributes of the table after the from index should be updated.
|
||||
///
|
||||
/// Before:
|
||||
/// ↓ reorder this column from index 1 to index 2
|
||||
/// | 0 | 1 | 2 |
|
||||
/// | 3 | 4 | 5 |
|
||||
///
|
||||
/// The original attributes of the table:
|
||||
/// {
|
||||
/// "columnColors": {
|
||||
/// 0: "#FF0000",
|
||||
/// 1: "#00FF00",
|
||||
/// 2: "#0000FF",
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// After reordering:
|
||||
/// | 0 | 2 | 1 |
|
||||
/// | 3 | 5 | 4 |
|
||||
///
|
||||
/// The new attributes of the table:
|
||||
/// {
|
||||
/// "columnColors": {
|
||||
/// 0: "#FF0000",
|
||||
/// 1: "#0000FF", ← The attributes of the original third column
|
||||
/// 2: "#00FF00", ← The attributes of the original second column
|
||||
/// }
|
||||
/// }
|
||||
Attributes? _mapColumnReorderingAttributes(int fromIndex, int toIndex) {
|
||||
final attributes = this.attributes;
|
||||
try {
|
||||
final duplicatedColumnColor = this.columnColors[fromIndex.toString()];
|
||||
final duplicatedColumnAlign = this.columnAligns[fromIndex.toString()];
|
||||
final duplicatedColumnWidth = this.columnWidths[fromIndex.toString()];
|
||||
|
||||
/// Case 1: fromIndex > toIndex
|
||||
/// Before:
|
||||
/// Row 0: | 0 | 1 | 2 |
|
||||
/// Row 1: | 3 | 4 | 5 |
|
||||
/// Row 2: | 6 | 7 | 8 |
|
||||
///
|
||||
/// columnColors = {
|
||||
/// "0": "#FF0000",
|
||||
/// "1": "#00FF00",
|
||||
/// "2": "#0000FF" ← Move this column (index 2)
|
||||
/// }
|
||||
///
|
||||
/// Move column 2 to index 0:
|
||||
/// Row 0: | 2 | 0 | 1 |
|
||||
/// Row 1: | 5 | 3 | 4 |
|
||||
/// Row 2: | 8 | 6 | 7 |
|
||||
///
|
||||
/// columnColors = {
|
||||
/// "0": "#0000FF", ← Moved here
|
||||
/// "1": "#FF0000",
|
||||
/// "2": "#00FF00"
|
||||
/// }
|
||||
///
|
||||
/// Case 2: fromIndex < toIndex
|
||||
/// Before:
|
||||
/// Row 0: | 0 | 1 | 2 |
|
||||
/// Row 1: | 3 | 4 | 5 |
|
||||
/// Row 2: | 6 | 7 | 8 |
|
||||
///
|
||||
/// columnColors = {
|
||||
/// "0": "#FF0000" ← Move this column (index 0)
|
||||
/// "1": "#00FF00",
|
||||
/// "2": "#0000FF"
|
||||
/// }
|
||||
///
|
||||
/// Move column 0 to index 2:
|
||||
/// Row 0: | 1 | 2 | 0 |
|
||||
/// Row 1: | 4 | 5 | 3 |
|
||||
/// Row 2: | 7 | 8 | 6 |
|
||||
///
|
||||
/// columnColors = {
|
||||
/// "0": "#00FF00",
|
||||
/// "1": "#0000FF",
|
||||
/// "2": "#FF0000" ← Moved here
|
||||
/// }
|
||||
final columnColors = _remapSource(
|
||||
this.columnColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnAligns = _remapSource(
|
||||
this.columnAligns,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final columnWidths = _remapSource(
|
||||
this.columnWidths,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnColors,
|
||||
columnColors,
|
||||
duplicatedEntry: duplicatedColumnColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnAligns,
|
||||
columnAligns,
|
||||
duplicatedEntry: duplicatedColumnAlign != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnAlign,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.columnWidths,
|
||||
columnWidths,
|
||||
duplicatedEntry: duplicatedColumnWidth != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedColumnWidth,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map column deletion attributes: $e');
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the attributes of a row reordering operation.
|
||||
///
|
||||
/// See [_mapColumnReorderingAttributes] for more details.
|
||||
Attributes? _mapRowReorderingAttributes(int fromIndex, int toIndex) {
|
||||
final attributes = this.attributes;
|
||||
try {
|
||||
final duplicatedRowColor = this.rowColors[fromIndex.toString()];
|
||||
final duplicatedRowAlign = this.rowAligns[fromIndex.toString()];
|
||||
|
||||
/// Example:
|
||||
/// Case 1: fromIndex > toIndex
|
||||
/// Before:
|
||||
/// Row 0: | 0 | 1 | 2 |
|
||||
/// Row 1: | 3 | 4 | 5 | ← Move this row (index 1)
|
||||
/// Row 2: | 6 | 7 | 8 |
|
||||
///
|
||||
/// rowColors = {
|
||||
/// "0": "#FF0000",
|
||||
/// "1": "#00FF00", ← This will be moved
|
||||
/// "2": "#0000FF"
|
||||
/// }
|
||||
///
|
||||
/// Move row 1 to index 0:
|
||||
/// Row 0: | 3 | 4 | 5 | ← Moved here
|
||||
/// Row 1: | 0 | 1 | 2 |
|
||||
/// Row 2: | 6 | 7 | 8 |
|
||||
///
|
||||
/// rowColors = {
|
||||
/// "0": "#00FF00", ← Moved here
|
||||
/// "1": "#FF0000",
|
||||
/// "2": "#0000FF"
|
||||
/// }
|
||||
///
|
||||
/// Case 2: fromIndex < toIndex
|
||||
/// Before:
|
||||
/// Row 0: | 0 | 1 | 2 |
|
||||
/// Row 1: | 3 | 4 | 5 | ← Move this row (index 1)
|
||||
/// Row 2: | 6 | 7 | 8 |
|
||||
///
|
||||
/// rowColors = {
|
||||
/// "0": "#FF0000",
|
||||
/// "1": "#00FF00", ← This will be moved
|
||||
/// "2": "#0000FF"
|
||||
/// }
|
||||
///
|
||||
/// Move row 1 to index 2:
|
||||
/// Row 0: | 0 | 1 | 2 |
|
||||
/// Row 1: | 3 | 4 | 5 |
|
||||
/// Row 2: | 6 | 7 | 8 | ← Moved here
|
||||
///
|
||||
/// rowColors = {
|
||||
/// "0": "#FF0000",
|
||||
/// "1": "#0000FF",
|
||||
/// "2": "#00FF00" ← Moved here
|
||||
/// }
|
||||
final rowColors = _remapSource(
|
||||
this.rowColors,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
final rowAligns = _remapSource(
|
||||
this.rowAligns,
|
||||
fromIndex,
|
||||
increment: fromIndex > toIndex,
|
||||
comparator: (iKey, index) {
|
||||
if (fromIndex > toIndex) {
|
||||
return iKey < fromIndex && iKey >= toIndex;
|
||||
} else {
|
||||
return iKey > fromIndex && iKey <= toIndex;
|
||||
}
|
||||
},
|
||||
filterIndex: fromIndex,
|
||||
);
|
||||
|
||||
return attributes
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowColors,
|
||||
rowColors,
|
||||
duplicatedEntry: duplicatedRowColor != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowColor,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
)
|
||||
.mergeValues(
|
||||
SimpleTableBlockKeys.rowAligns,
|
||||
rowAligns,
|
||||
duplicatedEntry: duplicatedRowAlign != null
|
||||
? MapEntry(
|
||||
toIndex.toString(),
|
||||
duplicatedRowAlign,
|
||||
)
|
||||
: null,
|
||||
removeNullValue: true,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.warn('Failed to map row reordering attributes: $e');
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the duplicated entry and remap the source.
|
||||
@ -486,6 +814,7 @@ extension TableMapOperationAttributes on Attributes {
|
||||
String key,
|
||||
Map<String, dynamic> newSource, {
|
||||
MapEntry? duplicatedEntry,
|
||||
bool removeNullValue = false,
|
||||
}) {
|
||||
final result = {...this};
|
||||
|
||||
@ -493,6 +822,11 @@ extension TableMapOperationAttributes on Attributes {
|
||||
newSource[duplicatedEntry.key] = duplicatedEntry.value;
|
||||
}
|
||||
|
||||
if (removeNullValue) {
|
||||
// remove the null value
|
||||
newSource.removeWhere((key, value) => value == null);
|
||||
}
|
||||
|
||||
result[key] = newSource;
|
||||
|
||||
return result;
|
||||
|
||||
@ -514,4 +514,19 @@ extension TableNodeExtension on Node {
|
||||
|
||||
return children[rowIndex].children[columnIndex];
|
||||
}
|
||||
|
||||
String? getTableCellContent({
|
||||
required int rowIndex,
|
||||
required int columnIndex,
|
||||
}) {
|
||||
final cell = getTableCellNode(rowIndex: rowIndex, columnIndex: columnIndex);
|
||||
if (cell == null) {
|
||||
return null;
|
||||
}
|
||||
final content = cell.children
|
||||
.map((e) => e.delta?.toPlainText())
|
||||
.where((e) => e != null)
|
||||
.join('\n');
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,4 +5,5 @@ export 'simple_table_header_operation.dart';
|
||||
export 'simple_table_insert_operation.dart';
|
||||
export 'simple_table_map_operation.dart';
|
||||
export 'simple_table_node_extension.dart';
|
||||
export 'simple_table_reorder_operation.dart';
|
||||
export 'simple_table_style_operation.dart';
|
||||
|
||||
@ -0,0 +1,120 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension SimpleTableReorderOperation on EditorState {
|
||||
/// Reorder the column of the table.
|
||||
///
|
||||
/// If the from index is equal to the to index, do nothing.
|
||||
/// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type].
|
||||
Future<void> reorderColumn(
|
||||
Node node, {
|
||||
required int fromIndex,
|
||||
required int toIndex,
|
||||
}) async {
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tableNode = node.parentTableNode;
|
||||
|
||||
if (tableNode == null) {
|
||||
assert(tableNode == null);
|
||||
return;
|
||||
}
|
||||
|
||||
final columnLength = tableNode.columnLength;
|
||||
final rowLength = tableNode.rowLength;
|
||||
|
||||
if (fromIndex < 0 ||
|
||||
fromIndex >= columnLength ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= columnLength) {
|
||||
Log.warn(
|
||||
'reorder column: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'reorder column in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength',
|
||||
);
|
||||
|
||||
final attributes = tableNode.mapTableAttributes(
|
||||
tableNode,
|
||||
type: TableMapOperationType.reorderColumn,
|
||||
index: fromIndex,
|
||||
toIndex: toIndex,
|
||||
);
|
||||
|
||||
final transaction = this.transaction;
|
||||
for (var i = 0; i < rowLength; i++) {
|
||||
final row = tableNode.children[i];
|
||||
final from = row.children[fromIndex];
|
||||
final to = row.children[toIndex];
|
||||
final path = fromIndex < toIndex ? to.path.next : to.path;
|
||||
transaction.insertNode(path, from.copyWith());
|
||||
transaction.deleteNode(from);
|
||||
}
|
||||
if (attributes != null) {
|
||||
transaction.updateNode(tableNode, attributes);
|
||||
}
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
/// Reorder the row of the table.
|
||||
///
|
||||
/// If the from index is equal to the to index, do nothing.
|
||||
/// The node's type can be [SimpleTableCellBlockKeys.type] or [SimpleTableRowBlockKeys.type] or [SimpleTableBlockKeys.type].
|
||||
Future<void> reorderRow(
|
||||
Node node, {
|
||||
required int fromIndex,
|
||||
required int toIndex,
|
||||
}) async {
|
||||
if (fromIndex == toIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tableNode = node.parentTableNode;
|
||||
|
||||
if (tableNode == null) {
|
||||
assert(tableNode == null);
|
||||
return;
|
||||
}
|
||||
|
||||
final columnLength = tableNode.columnLength;
|
||||
final rowLength = tableNode.rowLength;
|
||||
|
||||
if (fromIndex < 0 ||
|
||||
fromIndex >= rowLength ||
|
||||
toIndex < 0 ||
|
||||
toIndex >= rowLength) {
|
||||
Log.warn(
|
||||
'reorder row: index out of range: fromIndex: $fromIndex, toIndex: $toIndex, row length: $rowLength',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info(
|
||||
'reorder row in table ${node.id} at fromIndex: $fromIndex, toIndex: $toIndex, column length: $columnLength, row length: $rowLength',
|
||||
);
|
||||
|
||||
final attributes = tableNode.mapTableAttributes(
|
||||
tableNode,
|
||||
type: TableMapOperationType.reorderRow,
|
||||
index: fromIndex,
|
||||
toIndex: toIndex,
|
||||
);
|
||||
|
||||
final transaction = this.transaction;
|
||||
final from = tableNode.children[fromIndex];
|
||||
final to = tableNode.children[toIndex];
|
||||
final path = fromIndex < toIndex ? to.path.next : to.path;
|
||||
transaction.insertNode(path, from.copyWith());
|
||||
transaction.deleteNode(from);
|
||||
if (attributes != null) {
|
||||
transaction.updateNode(tableNode, attributes);
|
||||
}
|
||||
await apply(transaction);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,247 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SimpleTableBorderBuilder {
|
||||
SimpleTableBorderBuilder({
|
||||
required this.context,
|
||||
required this.simpleTableContext,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final SimpleTableContext simpleTableContext;
|
||||
final Node node;
|
||||
|
||||
/// Build the border for the cell.
|
||||
Border? buildBorder({bool isEditingCell = false}) {
|
||||
if (SimpleTableConstants.borderType != SimpleTableBorderRenderType.cell) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check if the cell is in the selected column
|
||||
final isCellInSelectedColumn =
|
||||
node.columnIndex == simpleTableContext.selectingColumn.value;
|
||||
|
||||
// check if the cell is in the selected row
|
||||
final isCellInSelectedRow =
|
||||
node.rowIndex == simpleTableContext.selectingRow.value;
|
||||
|
||||
// check if the cell is in the hovering column
|
||||
final isCellInHoveringColumn =
|
||||
simpleTableContext.hoveringTableCell.value?.columnIndex ==
|
||||
node.columnIndex;
|
||||
|
||||
final isCellInHoveringRow =
|
||||
simpleTableContext.hoveringTableCell.value?.rowIndex == node.rowIndex;
|
||||
|
||||
// check if the cell is in the reordering column
|
||||
final isReordering = simpleTableContext.isReordering;
|
||||
|
||||
if (isReordering && (isCellInHoveringColumn || isCellInHoveringRow)) {
|
||||
return buildReorderingBorder();
|
||||
} else if (simpleTableContext.isSelectingTable.value) {
|
||||
return buildSelectingTableBorder();
|
||||
} else if (isCellInSelectedColumn) {
|
||||
return buildColumnHighlightBorder();
|
||||
} else if (isCellInSelectedRow) {
|
||||
return buildRowHighlightBorder();
|
||||
} else if (isEditingCell) {
|
||||
return buildEditingBorder();
|
||||
} else {
|
||||
return buildCellBorder();
|
||||
}
|
||||
}
|
||||
|
||||
/// the column border means the `VERTICAL` border of the cell
|
||||
///
|
||||
/// ____
|
||||
/// | 1 | 2 |
|
||||
/// | 3 | 4 |
|
||||
/// |___|
|
||||
///
|
||||
/// the border wrapping the cell 2 and cell 4 is the column border
|
||||
Border buildColumnHighlightBorder() {
|
||||
return Border(
|
||||
left: _buildHighlightBorderSide(),
|
||||
right: _buildHighlightBorderSide(),
|
||||
top: node.rowIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
/// the row border means the `HORIZONTAL` border of the cell
|
||||
///
|
||||
/// ________
|
||||
/// | 1 | 2 |
|
||||
/// |_______|
|
||||
/// | 3 | 4 |
|
||||
///
|
||||
/// the border wrapping the cell 1 and cell 2 is the row border
|
||||
Border buildRowHighlightBorder() {
|
||||
return Border(
|
||||
top: _buildHighlightBorderSide(),
|
||||
bottom: _buildHighlightBorderSide(),
|
||||
left: node.columnIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
right: node.columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the border for the reordering state.
|
||||
///
|
||||
/// For example, when reordering a column, we should highlight the border of the
|
||||
/// current column we're hovering.
|
||||
Border buildReorderingBorder() {
|
||||
final isReorderingColumn = simpleTableContext.isReorderingColumn.value.$1;
|
||||
final isReorderingRow = simpleTableContext.isReorderingRow.value.$1;
|
||||
|
||||
if (isReorderingColumn) {
|
||||
return _buildColumnReorderingBorder();
|
||||
} else if (isReorderingRow) {
|
||||
return _buildRowReorderingBorder();
|
||||
}
|
||||
|
||||
return buildCellBorder();
|
||||
}
|
||||
|
||||
/// Build the border for the cell without any state.
|
||||
Border buildCellBorder() {
|
||||
return Border(
|
||||
top: node.rowIndex == 0
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
left: node.columnIndex == 0
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
right: node.columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildDefaultBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the border for the editing state.
|
||||
Border buildEditingBorder() {
|
||||
return Border.all(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the border for the selecting table state.
|
||||
Border buildSelectingTableBorder() {
|
||||
final rowIndex = node.rowIndex;
|
||||
final columnIndex = node.columnIndex;
|
||||
|
||||
return Border(
|
||||
top:
|
||||
rowIndex == 0 ? _buildHighlightBorderSide() : _buildLightBorderSide(),
|
||||
bottom: rowIndex + 1 == node.parentTableNode?.rowLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
left: columnIndex == 0
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
right: columnIndex + 1 == node.parentTableNode?.columnLength
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildLightBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
Border _buildColumnReorderingBorder() {
|
||||
assert(simpleTableContext.isReordering);
|
||||
|
||||
final isDraggingInCurrentColumn =
|
||||
simpleTableContext.isReorderingColumn.value.$2 == node.columnIndex;
|
||||
// if the dragging column is the current column, don't show the highlight border
|
||||
if (isDraggingInCurrentColumn) {
|
||||
return buildCellBorder();
|
||||
}
|
||||
|
||||
final hoveringTableCell = simpleTableContext.hoveringTableCell.value;
|
||||
// if the hovering column is not the current column, don't show the highlight border
|
||||
final isHitCurrentCell = hoveringTableCell?.columnIndex == node.columnIndex;
|
||||
if (!isHitCurrentCell) {
|
||||
return buildCellBorder();
|
||||
}
|
||||
|
||||
// if the dragging column index is less than the current column index, show the
|
||||
// highlight border on the left side
|
||||
final isLeftSide =
|
||||
simpleTableContext.isReorderingColumn.value.$2 > node.columnIndex;
|
||||
// if the dragging column index is greater than the current column index, show
|
||||
// the highlight border on the right side
|
||||
final isRightSide =
|
||||
simpleTableContext.isReorderingColumn.value.$2 < node.columnIndex;
|
||||
|
||||
return Border(
|
||||
top: _buildDefaultBorderSide(),
|
||||
bottom: _buildDefaultBorderSide(),
|
||||
left:
|
||||
isLeftSide ? _buildHighlightBorderSide() : _buildDefaultBorderSide(),
|
||||
right:
|
||||
isRightSide ? _buildHighlightBorderSide() : _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
Border _buildRowReorderingBorder() {
|
||||
assert(simpleTableContext.isReordering);
|
||||
|
||||
final isDraggingInCurrentRow =
|
||||
simpleTableContext.isReorderingRow.value.$2 == node.rowIndex;
|
||||
// if the dragging row is the current row, don't show the highlight border
|
||||
if (isDraggingInCurrentRow) {
|
||||
return buildCellBorder();
|
||||
}
|
||||
|
||||
final hoveringTableCell = simpleTableContext.hoveringTableCell.value;
|
||||
final isHitCurrentCell = hoveringTableCell?.rowIndex == node.rowIndex;
|
||||
if (!isHitCurrentCell) {
|
||||
return buildCellBorder();
|
||||
}
|
||||
|
||||
final isTopSide =
|
||||
simpleTableContext.isReorderingRow.value.$2 > node.rowIndex;
|
||||
final isBottomSide =
|
||||
simpleTableContext.isReorderingRow.value.$2 < node.rowIndex;
|
||||
|
||||
return Border(
|
||||
top: isTopSide ? _buildHighlightBorderSide() : _buildDefaultBorderSide(),
|
||||
bottom: isBottomSide
|
||||
? _buildHighlightBorderSide()
|
||||
: _buildDefaultBorderSide(),
|
||||
left: _buildDefaultBorderSide(),
|
||||
right: _buildDefaultBorderSide(),
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildHighlightBorderSide() {
|
||||
return BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildLightBorderSide() {
|
||||
return BorderSide(
|
||||
color: context.simpleTableBorderColor,
|
||||
width: 0.5,
|
||||
);
|
||||
}
|
||||
|
||||
BorderSide _buildDefaultBorderSide() {
|
||||
return BorderSide(
|
||||
color: context.simpleTableBorderColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SimpleTableColumnResizeHandle extends StatefulWidget {
|
||||
const SimpleTableColumnResizeHandle({
|
||||
super.key,
|
||||
required this.node,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
|
||||
@override
|
||||
State<SimpleTableColumnResizeHandle> createState() =>
|
||||
_SimpleTableColumnResizeHandleState();
|
||||
}
|
||||
|
||||
class _SimpleTableColumnResizeHandleState
|
||||
extends State<SimpleTableColumnResizeHandle> {
|
||||
bool isStartDragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final simpleTableContext = context.read<SimpleTableContext>();
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.resizeColumn,
|
||||
onEnter: (_) => _onEnterHoverArea(),
|
||||
onExit: (event) => _onExitHoverArea(),
|
||||
child: GestureDetector(
|
||||
onHorizontalDragStart: _onHorizontalDragStart,
|
||||
onHorizontalDragUpdate: _onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: _onHorizontalDragEnd,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: simpleTableContext.hoveringOnResizeHandle,
|
||||
builder: (context, hoveringOnResizeHandle, child) {
|
||||
// when reordering a column, the resize handle should not be shown
|
||||
final isSameRowIndex = hoveringOnResizeHandle?.columnIndex ==
|
||||
widget.node.columnIndex &&
|
||||
!simpleTableContext.isReordering;
|
||||
return Opacity(
|
||||
opacity: isSameRowIndex ? 1.0 : 0.0,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: double.infinity,
|
||||
width: SimpleTableConstants.resizeHandleWidth,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onEnterHoverArea() {
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle.value =
|
||||
widget.node;
|
||||
}
|
||||
|
||||
void _onExitHoverArea() {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
// the onExit event will be triggered before dragging started.
|
||||
// delay the hiding of the resize handle to avoid flickering.
|
||||
if (!isStartDragging) {
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle.value = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onHorizontalDragStart(DragStartDetails details) {
|
||||
// disable the two-finger drag on trackpad
|
||||
if (details.kind == PointerDeviceKind.trackpad) {
|
||||
return;
|
||||
}
|
||||
|
||||
isStartDragging = true;
|
||||
}
|
||||
|
||||
void _onHorizontalDragUpdate(DragUpdateDetails details) {
|
||||
if (!isStartDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// only update the column width in memory,
|
||||
// the actual update will be applied in _onHorizontalDragEnd
|
||||
context.read<EditorState>().updateColumnWidthInMemory(
|
||||
tableCellNode: widget.node,
|
||||
deltaX: details.delta.dx,
|
||||
);
|
||||
}
|
||||
|
||||
void _onHorizontalDragEnd(DragEndDetails details) {
|
||||
if (!isStartDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
isStartDragging = false;
|
||||
context.read<SimpleTableContext>().hoveringOnResizeHandle.value = null;
|
||||
|
||||
// apply the updated column width
|
||||
context.read<EditorState>().updateColumnWidth(
|
||||
tableCellNode: widget.node,
|
||||
width: widget.node.columnWidth,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,258 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/simple_table_widget.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SimpleTableDraggableReorderButton extends StatelessWidget {
|
||||
const SimpleTableDraggableReorderButton({
|
||||
super.key,
|
||||
required this.node,
|
||||
required this.index,
|
||||
required this.isShowingMenu,
|
||||
required this.type,
|
||||
required this.editorState,
|
||||
required this.simpleTableContext,
|
||||
});
|
||||
|
||||
final Node node;
|
||||
final int index;
|
||||
final ValueNotifier<bool> isShowingMenu;
|
||||
final SimpleTableMoreActionType type;
|
||||
final EditorState editorState;
|
||||
final SimpleTableContext simpleTableContext;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Draggable<int>(
|
||||
data: index,
|
||||
onDragStarted: () => _startDragging(),
|
||||
onDragUpdate: (details) => _onDragUpdate(details),
|
||||
onDragEnd: (_) => _stopDragging(),
|
||||
feedback: SimpleTableFeedback(
|
||||
editorState: editorState,
|
||||
node: node,
|
||||
type: type,
|
||||
index: index,
|
||||
),
|
||||
child: SimpleTableReorderButton(
|
||||
isShowingMenu: isShowingMenu,
|
||||
type: type,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _startDragging() {
|
||||
switch (type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
simpleTableContext.isReorderingColumn.value = (true, index);
|
||||
break;
|
||||
case SimpleTableMoreActionType.row:
|
||||
simpleTableContext.isReorderingRow.value = (true, index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _onDragUpdate(DragUpdateDetails details) {
|
||||
simpleTableContext.reorderingOffset.value = details.globalPosition;
|
||||
}
|
||||
|
||||
void _stopDragging() {
|
||||
switch (type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
_reorderColumn();
|
||||
case SimpleTableMoreActionType.row:
|
||||
_reorderRow();
|
||||
}
|
||||
|
||||
simpleTableContext.reorderingOffset.value = Offset.zero;
|
||||
switch (type) {
|
||||
case SimpleTableMoreActionType.column:
|
||||
simpleTableContext.isReorderingColumn.value = (false, -1);
|
||||
break;
|
||||
case SimpleTableMoreActionType.row:
|
||||
simpleTableContext.isReorderingRow.value = (false, -1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _reorderColumn() {
|
||||
final fromIndex = simpleTableContext.isReorderingColumn.value.$2;
|
||||
final toIndex = simpleTableContext.hoveringTableCell.value?.columnIndex;
|
||||
if (toIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.reorderColumn(
|
||||
node,
|
||||
fromIndex: fromIndex,
|
||||
toIndex: toIndex,
|
||||
);
|
||||
}
|
||||
|
||||
void _reorderRow() {
|
||||
final fromIndex = simpleTableContext.isReorderingRow.value.$2;
|
||||
final toIndex = simpleTableContext.hoveringTableCell.value?.rowIndex;
|
||||
if (toIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.reorderRow(
|
||||
node,
|
||||
fromIndex: fromIndex,
|
||||
toIndex: toIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableReorderButton extends StatelessWidget {
|
||||
const SimpleTableReorderButton({
|
||||
super.key,
|
||||
required this.isShowingMenu,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final ValueNotifier<bool> isShowingMenu;
|
||||
final SimpleTableMoreActionType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isShowingMenu,
|
||||
builder: (context, isShowingMenu, child) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isShowingMenu
|
||||
? context.simpleTableMoreActionHoverColor
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
border: Border.all(
|
||||
color: context.simpleTableMoreActionBorderColor,
|
||||
),
|
||||
),
|
||||
height: 16.0,
|
||||
width: 16.0,
|
||||
child: FlowySvg(
|
||||
type.reorderIconSvg,
|
||||
color: isShowingMenu ? Colors.white : null,
|
||||
size: const Size.square(16.0),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleTableFeedback extends StatefulWidget {
|
||||
const SimpleTableFeedback({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.node,
|
||||
required this.type,
|
||||
required this.index,
|
||||
});
|
||||
|
||||
/// The node of the table.
|
||||
/// Its type must be [SimpleTableBlockKeys.type].
|
||||
final Node node;
|
||||
|
||||
/// The type of the more action.
|
||||
///
|
||||
/// If the type is [SimpleTableMoreActionType.column], the feedback will use index as column index.
|
||||
/// If the type is [SimpleTableMoreActionType.row], the feedback will use index as row index.
|
||||
final SimpleTableMoreActionType type;
|
||||
|
||||
/// The index of the column or row.
|
||||
final int index;
|
||||
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<SimpleTableFeedback> createState() => _SimpleTableFeedbackState();
|
||||
}
|
||||
|
||||
class _SimpleTableFeedbackState extends State<SimpleTableFeedback> {
|
||||
final simpleTableContext = SimpleTableContext();
|
||||
late final Node dummyNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
simpleTableContext.isSelectingTable.value = true;
|
||||
dummyNode = _buildDummyNode();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
simpleTableContext.dispose();
|
||||
dummyNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider.value(
|
||||
value: widget.editorState,
|
||||
child: SimpleTableWidget(
|
||||
node: dummyNode,
|
||||
simpleTableContext: simpleTableContext,
|
||||
enableAddColumnButton: false,
|
||||
enableAddRowButton: false,
|
||||
enableAddColumnAndRowButton: false,
|
||||
enableHoverEffect: false,
|
||||
isFeedback: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the dummy node for the feedback.
|
||||
///
|
||||
/// For example,
|
||||
///
|
||||
/// If the type is [SimpleTableMoreActionType.row], we should build the dummy table node using the data from the first row of the table node.
|
||||
/// If the type is [SimpleTableMoreActionType.column], we should build the dummy table node using the data from the first column of the table node.
|
||||
Node _buildDummyNode() {
|
||||
// deep copy the table node to avoid mutating the original node
|
||||
final tableNode = widget.node.copyWith();
|
||||
|
||||
switch (widget.type) {
|
||||
case SimpleTableMoreActionType.row:
|
||||
if (widget.index >= tableNode.rowLength || widget.index < 0) {
|
||||
return simpleTableBlockNode(children: []);
|
||||
}
|
||||
|
||||
final row = tableNode.children[widget.index];
|
||||
return tableNode.copyWith(
|
||||
children: [row],
|
||||
attributes: {
|
||||
...tableNode.attributes,
|
||||
if (widget.index != 0) SimpleTableBlockKeys.enableHeaderRow: false,
|
||||
},
|
||||
);
|
||||
case SimpleTableMoreActionType.column:
|
||||
if (widget.index >= tableNode.columnLength || widget.index < 0) {
|
||||
return simpleTableBlockNode(children: []);
|
||||
}
|
||||
|
||||
final rows = tableNode.children.map((row) {
|
||||
final cell = row.children[widget.index];
|
||||
return simpleTableRowBlockNode(children: [cell]);
|
||||
}).toList();
|
||||
|
||||
return tableNode.copyWith(
|
||||
children: rows,
|
||||
attributes: {
|
||||
...tableNode.attributes,
|
||||
if (widget.index != 0)
|
||||
SimpleTableBlockKeys.enableHeaderColumn: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
class SimpleTableWidget extends StatefulWidget {
|
||||
const SimpleTableWidget({
|
||||
super.key,
|
||||
required this.simpleTableContext,
|
||||
required this.node,
|
||||
this.enableAddColumnButton = true,
|
||||
this.enableAddRowButton = true,
|
||||
this.enableAddColumnAndRowButton = true,
|
||||
this.enableHoverEffect = true,
|
||||
this.isFeedback = false,
|
||||
});
|
||||
|
||||
/// The node of the table.
|
||||
///
|
||||
/// Its type must be [SimpleTableBlockKeys.type].
|
||||
final Node node;
|
||||
|
||||
/// The context of the simple table.
|
||||
final SimpleTableContext simpleTableContext;
|
||||
|
||||
/// Whether to show the add column button.
|
||||
///
|
||||
/// For the feedback widget builder, it should be false.
|
||||
final bool enableAddColumnButton;
|
||||
|
||||
/// Whether to show the add row button.
|
||||
///
|
||||
/// For the feedback widget builder, it should be false.
|
||||
final bool enableAddRowButton;
|
||||
|
||||
/// Whether to show the add column and row button.
|
||||
///
|
||||
/// For the feedback widget builder, it should be false.
|
||||
final bool enableAddColumnAndRowButton;
|
||||
|
||||
/// Whether to enable the hover effect.
|
||||
///
|
||||
/// For the feedback widget builder, it should be false.
|
||||
final bool enableHoverEffect;
|
||||
|
||||
/// Whether the widget is a feedback widget.
|
||||
final bool isFeedback;
|
||||
|
||||
@override
|
||||
State<SimpleTableWidget> createState() => _SimpleTableWidgetState();
|
||||
}
|
||||
|
||||
class _SimpleTableWidgetState extends State<SimpleTableWidget> {
|
||||
SimpleTableContext get simpleTableContext => widget.simpleTableContext;
|
||||
|
||||
final scrollController = ScrollController();
|
||||
late final editorState = context.read<EditorState>();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return UniversalPlatform.isDesktop
|
||||
? _buildDesktopTable()
|
||||
: _buildMobileTable();
|
||||
}
|
||||
|
||||
Widget _buildDesktopTable() {
|
||||
if (widget.isFeedback) {
|
||||
return Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: IntrinsicWidth(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildRows(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// table content
|
||||
Widget child = Scrollbar(
|
||||
controller: scrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: SimpleTableConstants.tablePadding,
|
||||
// IntrinsicWidth and IntrinsicHeight are used to make the table size fit the content.
|
||||
child: IntrinsicWidth(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildRows(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.enableHoverEffect) {
|
||||
child = MouseRegion(
|
||||
onEnter: (event) =>
|
||||
simpleTableContext.isHoveringOnTableArea.value = true,
|
||||
onExit: (event) {
|
||||
simpleTableContext.isHoveringOnTableArea.value = false;
|
||||
},
|
||||
child: Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: Stack(
|
||||
children: [
|
||||
MouseRegion(
|
||||
hitTestBehavior: HitTestBehavior.opaque,
|
||||
onEnter: (event) =>
|
||||
simpleTableContext.isHoveringOnColumnsAndRows.value = true,
|
||||
onExit: (event) {
|
||||
simpleTableContext.isHoveringOnColumnsAndRows.value = false;
|
||||
simpleTableContext.hoveringTableCell.value = null;
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
if (widget.enableAddColumnButton)
|
||||
SimpleTableAddColumnHoverButton(
|
||||
editorState: editorState,
|
||||
node: widget.node,
|
||||
),
|
||||
if (widget.enableAddRowButton)
|
||||
SimpleTableAddRowHoverButton(
|
||||
editorState: editorState,
|
||||
tableNode: widget.node,
|
||||
),
|
||||
if (widget.enableAddColumnAndRowButton)
|
||||
SimpleTableAddColumnAndRowHoverButton(
|
||||
editorState: editorState,
|
||||
node: widget.node,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildMobileTable() {
|
||||
return Provider.value(
|
||||
value: simpleTableContext,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollController,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: IntrinsicWidth(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: _buildRows(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildRows() {
|
||||
final List<Widget> rows = [];
|
||||
|
||||
if (SimpleTableConstants.borderType == SimpleTableBorderRenderType.table) {
|
||||
rows.add(const SimpleTableColumnDivider());
|
||||
}
|
||||
|
||||
for (final child in widget.node.children) {
|
||||
rows.add(editorState.renderer.build(context, child));
|
||||
|
||||
if (SimpleTableConstants.borderType ==
|
||||
SimpleTableBorderRenderType.table) {
|
||||
rows.add(const SimpleTableColumnDivider());
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,335 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'simple_table_test_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('Simple table reorder operation:', () {
|
||||
setUpAll(() {
|
||||
Log.shared.disableLog = true;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
Log.shared.disableLog = false;
|
||||
});
|
||||
|
||||
group('reorder column', () {
|
||||
test('reorder column from index 1 to index 2', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 4,
|
||||
columnCount: 3,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 2);
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 4);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),
|
||||
'cell 0-2',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),
|
||||
'cell 0-1',
|
||||
);
|
||||
});
|
||||
|
||||
test('reorder column from index 2 to index 0', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 4,
|
||||
columnCount: 3,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0);
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 4);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 0-2',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),
|
||||
'cell 0-1',
|
||||
);
|
||||
});
|
||||
|
||||
test('reorder column with same index', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 4,
|
||||
columnCount: 3,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 1, toIndex: 1);
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 4);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 1),
|
||||
'cell 0-1',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 2),
|
||||
'cell 0-2',
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'reorder column from index 0 to index 2 with align/color/width attributes (1)',
|
||||
() async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 4,
|
||||
columnCount: 3,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
|
||||
// before reorder
|
||||
// Column 0: align: right, color: 0xFF0000, width: 100
|
||||
// Column 1: align: center, color: 0x00FF00, width: 150
|
||||
// Column 2: align: left, color: 0x0000FF, width: 200
|
||||
await updateTableColumnAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
columnIndex: 0,
|
||||
align: TableAlign.right,
|
||||
color: '#FF0000',
|
||||
width: 100,
|
||||
);
|
||||
await updateTableColumnAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
columnIndex: 1,
|
||||
align: TableAlign.center,
|
||||
color: '#00FF00',
|
||||
width: 150,
|
||||
);
|
||||
await updateTableColumnAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
columnIndex: 2,
|
||||
align: TableAlign.left,
|
||||
color: '#0000FF',
|
||||
width: 200,
|
||||
);
|
||||
|
||||
// after reorder
|
||||
// Column 0: align: center, color: 0x00FF00, width: 150
|
||||
// Column 1: align: left, color: 0x0000FF, width: 200
|
||||
// Column 2: align: right, color: 0xFF0000, width: 100
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2);
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 4);
|
||||
|
||||
expect(tableNode.columnAligns, {
|
||||
"0": TableAlign.center.key,
|
||||
"1": TableAlign.left.key,
|
||||
"2": TableAlign.right.key,
|
||||
});
|
||||
expect(tableNode.columnColors, {
|
||||
"0": '#00FF00',
|
||||
"1": '#0000FF',
|
||||
"2": '#FF0000',
|
||||
});
|
||||
expect(tableNode.columnWidths, {
|
||||
"0": 150,
|
||||
"1": 200,
|
||||
"2": 100,
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'reorder column from index 0 to index 2 and reorder it back to index 0',
|
||||
() async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 2,
|
||||
columnCount: 3,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
|
||||
// before reorder
|
||||
// Column 0: null
|
||||
// Column 1: align: center, color: 0x0000FF, width: 200
|
||||
// Column 2: align: right, color: 0x0000FF, width: 250
|
||||
await updateTableColumnAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
columnIndex: 1,
|
||||
align: TableAlign.center,
|
||||
color: '#FF0000',
|
||||
width: 200,
|
||||
);
|
||||
await updateTableColumnAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
columnIndex: 2,
|
||||
align: TableAlign.right,
|
||||
color: '#0000FF',
|
||||
width: 250,
|
||||
);
|
||||
|
||||
// move column from index 0 to index 2
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 0, toIndex: 2);
|
||||
// move column from index 2 to index 0
|
||||
await editorState.reorderColumn(tableNode, fromIndex: 2, toIndex: 0);
|
||||
expect(tableNode.columnLength, 3);
|
||||
expect(tableNode.rowLength, 2);
|
||||
|
||||
expect(tableNode.columnAligns, {
|
||||
"1": TableAlign.center.key,
|
||||
"2": TableAlign.right.key,
|
||||
});
|
||||
expect(tableNode.columnColors, {
|
||||
"1": '#FF0000',
|
||||
"2": '#0000FF',
|
||||
});
|
||||
expect(tableNode.columnWidths, {
|
||||
"1": 200,
|
||||
"2": 250,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('reorder row', () {
|
||||
test('reorder row from index 1 to index 2', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 2,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 2);
|
||||
expect(tableNode.columnLength, 2);
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),
|
||||
'cell 2-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),
|
||||
'cell 1-0',
|
||||
);
|
||||
});
|
||||
|
||||
test('reorder row from index 2 to index 0', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 2,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderRow(tableNode, fromIndex: 2, toIndex: 0);
|
||||
expect(tableNode.columnLength, 2);
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 2-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),
|
||||
'cell 1-0',
|
||||
);
|
||||
});
|
||||
|
||||
test('reorder row with same', () async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 2,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
await editorState.reorderRow(tableNode, fromIndex: 1, toIndex: 1);
|
||||
expect(tableNode.columnLength, 2);
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 0, columnIndex: 0),
|
||||
'cell 0-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 1, columnIndex: 0),
|
||||
'cell 1-0',
|
||||
);
|
||||
expect(
|
||||
tableNode.getTableCellContent(rowIndex: 2, columnIndex: 0),
|
||||
'cell 2-0',
|
||||
);
|
||||
});
|
||||
|
||||
test('reorder row from index 0 to index 2 with align/color attributes',
|
||||
() async {
|
||||
final (editorState, tableNode) = createEditorStateAndTable(
|
||||
rowCount: 3,
|
||||
columnCount: 2,
|
||||
contentBuilder: (rowIndex, columnIndex) =>
|
||||
'cell $rowIndex-$columnIndex',
|
||||
);
|
||||
|
||||
// before reorder
|
||||
// Row 0: align: right, color: 0xFF0000
|
||||
// Row 1: align: center, color: 0x00FF00
|
||||
// Row 2: align: left, color: 0x0000FF
|
||||
await updateTableRowAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
rowIndex: 0,
|
||||
align: TableAlign.right,
|
||||
color: '#FF0000',
|
||||
);
|
||||
await updateTableRowAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
rowIndex: 1,
|
||||
align: TableAlign.center,
|
||||
color: '#00FF00',
|
||||
);
|
||||
await updateTableRowAttributes(
|
||||
editorState,
|
||||
tableNode,
|
||||
rowIndex: 2,
|
||||
align: TableAlign.left,
|
||||
color: '#0000FF',
|
||||
);
|
||||
|
||||
// after reorder
|
||||
// Row 0: align: center, color: 0x00FF00
|
||||
// Row 1: align: left, color: 0x0000FF
|
||||
// Row 2: align: right, color: 0xFF0000
|
||||
await editorState.reorderRow(tableNode, fromIndex: 0, toIndex: 2);
|
||||
expect(tableNode.columnLength, 2);
|
||||
expect(tableNode.rowLength, 3);
|
||||
expect(tableNode.rowAligns, {
|
||||
"0": TableAlign.center.key,
|
||||
"1": TableAlign.left.key,
|
||||
"2": TableAlign.right.key,
|
||||
});
|
||||
expect(tableNode.rowColors, {
|
||||
"0": '#00FF00',
|
||||
"1": '#0000FF',
|
||||
"2": '#FF0000',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
(EditorState editorState, Node tableNode) createEditorStateAndTable({
|
||||
required int rowCount,
|
||||
required int columnCount,
|
||||
String? defaultContent,
|
||||
String Function(int rowIndex, int columnIndex)? contentBuilder,
|
||||
}) {
|
||||
final document = Document.blank()
|
||||
..insert(
|
||||
@ -14,9 +15,72 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
columnCount: columnCount,
|
||||
rowCount: rowCount,
|
||||
defaultContent: defaultContent,
|
||||
contentBuilder: contentBuilder,
|
||||
),
|
||||
],
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
return (editorState, document.nodeAtPath([0])!);
|
||||
}
|
||||
|
||||
Future<void> updateTableColumnAttributes(
|
||||
EditorState editorState,
|
||||
Node tableNode, {
|
||||
required int columnIndex,
|
||||
TableAlign? align,
|
||||
String? color,
|
||||
double? width,
|
||||
}) async {
|
||||
final cell = tableNode.getTableCellNode(
|
||||
rowIndex: 0,
|
||||
columnIndex: columnIndex,
|
||||
)!;
|
||||
|
||||
if (align != null) {
|
||||
await editorState.updateColumnAlign(
|
||||
tableCellNode: cell,
|
||||
align: align,
|
||||
);
|
||||
}
|
||||
|
||||
if (color != null) {
|
||||
await editorState.updateColumnBackgroundColor(
|
||||
tableCellNode: cell,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
|
||||
if (width != null) {
|
||||
await editorState.updateColumnWidth(
|
||||
tableCellNode: cell,
|
||||
width: width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateTableRowAttributes(
|
||||
EditorState editorState,
|
||||
Node tableNode, {
|
||||
required int rowIndex,
|
||||
TableAlign? align,
|
||||
String? color,
|
||||
}) async {
|
||||
final cell = tableNode.getTableCellNode(
|
||||
rowIndex: rowIndex,
|
||||
columnIndex: 0,
|
||||
)!;
|
||||
|
||||
if (align != null) {
|
||||
await editorState.updateRowAlign(
|
||||
tableCellNode: cell,
|
||||
align: align,
|
||||
);
|
||||
}
|
||||
|
||||
if (color != null) {
|
||||
await editorState.updateRowBackgroundColor(
|
||||
tableCellNode: cell,
|
||||
color: color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user