feat: simple table issues (#6871)

* fix: disable cut command in table cell

* feat: only keep the table cell content when coping text from table

* fix: focus on the first cell after inserting table

* test: focus on the first cell after inserting table

* feat: highlight the cell when editing

* test: highlight the cell when editing

* fix: creating a new row makes a cursor appear for a fraction of a second

* fix: add 4px between scroll bar and add row button

* chore: rename simple table components

* fix: select all in table cell block

* test: select all in table cell block

* feat: disable two-fingers resize in table cell

* feat: includ table when exporting markdown

* test: include table when exporting markdown

* feat: optimize add row button render logic

* chore: optimize hover button render logic

* fix: column button is not clickable

* fix: theme assertion

* feat: improve hovering logic

* fix: selection issue in table

* fix(flutter_desktop): popover conflicts on simple table

* feat: support table markdown import

* test: table cell isEditing test

* test: select all in table test

* fix: popover conflict in table action menu

* test: insert row, column, row/column in table

* test: delete row, column, row/column in table

* test: enable header column and header row in table

* test: duplicate/insert left/right/above/below in table

* chore: duplicate table in optin menu

* fix: integraion test

---------

Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
Lucas 2024-12-02 17:50:32 +08:00 committed by GitHub
parent 550b8835c6
commit e7491e5182
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 1465 additions and 457 deletions

View File

@ -1,8 +1,12 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
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:easy_localization/easy_localization.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:universal_platform/universal_platform.dart';
import '../../shared/util.dart';
@ -17,26 +21,350 @@ void main() {
testWidgets('insert a simple table block', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await insertTableInDocument(tester);
await tester.insertTableInDocument();
// validate the table is inserted
expect(find.byType(SimpleTableBlockWidget), findsOneWidget);
final editorState = tester.editor.getCurrentEditorState();
expect(
editorState.selection,
// table -> row -> cell -> paragraph
Selection.collapsed(Position(path: [0, 0, 0, 0])),
);
final firstCell = find.byType(SimpleTableCellBlockWidget).first;
expect(
tester
.state<SimpleTableCellBlockWidgetState>(firstCell)
.isEditingCellNotifier
.value,
isTrue,
);
});
testWidgets('select all in table cell', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
const cell1Content = 'Cell 1';
await tester.editor.tapLineOfEditorAt(0);
await tester.ime.insertText('New Table');
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
await tester.pumpAndSettle();
await tester.editor.tapLineOfEditorAt(1);
await tester.insertTableInDocument();
await tester.ime.insertText(cell1Content);
await tester.pumpAndSettle();
// Select all in the cell
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [1, 0, 0, 0]),
end: Position(path: [1, 0, 0, 0], offset: cell1Content.length),
),
);
// Press select all again, the selection should be the entire document
await tester.simulateKeyEvent(
LogicalKeyboardKey.keyA,
isControlPressed: !UniversalPlatform.isMacOS,
isMetaPressed: UniversalPlatform.isMacOS,
);
expect(
tester.editor.getCurrentEditorState().selection,
Selection(
start: Position(path: [0]),
end: Position(path: [1, 1, 1, 0]),
),
);
});
testWidgets('''
1. hover on the table
1.1 click the add row button
1.2 click the add column button
1.3 click the add row and column button
2. validate the table is updated
3. delete the last column
4. delete the last row
5. validate the table is updated
''', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// hover on the table
final tableBlock = find.byType(SimpleTableBlockWidget).first;
await tester.hoverOnWidget(
tableBlock,
onHover: () async {
// click the add row button
final addRowButton = find.byType(SimpleTableAddRowButton).first;
await tester.tap(addRowButton);
// click the add column button
final addColumnButton = find.byType(SimpleTableAddColumnButton).first;
await tester.tap(addColumnButton);
// click the add row and column button
final addRowAndColumnButton =
find.byType(SimpleTableAddColumnAndRowButton).first;
await tester.tap(addRowAndColumnButton);
},
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 4);
// delete the last row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: tableNode.rowLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.rowLength, 3);
expect(tableNode.columnLength, 4);
// delete the last column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: tableNode.columnLength - 1,
action: SimpleTableMoreAction.delete,
);
await tester.pumpAndSettle();
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('enable header column and header row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// enable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
// enable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isTrue);
// disable the header row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.enableHeaderRow,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isTrue);
expect(tableNode.isHeaderRowEnabled, isFalse);
// disable the header column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.enableHeaderColumn,
);
await tester.pumpAndSettle();
expect(tableNode.isHeaderColumnEnabled, isFalse);
expect(tableNode.isHeaderRowEnabled, isFalse);
});
testWidgets('duplicate a column / row', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// duplicate the row
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
// duplicate the column
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.duplicate,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 3);
expect(tableNode.rowLength, 3);
});
testWidgets('insert left / insert right', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert left
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertLeft,
);
await tester.pumpAndSettle();
// insert right
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.column,
index: 0,
action: SimpleTableMoreAction.insertRight,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.columnLength, 4);
expect(tableNode.rowLength, 2);
});
testWidgets('insert above / insert below', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
await tester.createNewPageWithNameUnderParent(
name: 'simple_table_test',
);
await tester.editor.tapLineOfEditorAt(0);
await tester.insertTableInDocument();
// insert above
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertAbove,
);
await tester.pumpAndSettle();
// insert below
await tester.clickMoreActionItemInTableMenu(
type: SimpleTableMoreActionType.row,
index: 0,
action: SimpleTableMoreAction.insertBelow,
);
await tester.pumpAndSettle();
final tableNode =
tester.editor.getCurrentEditorState().document.nodeAtPath([0])!;
expect(tableNode.rowLength, 4);
expect(tableNode.columnLength, 2);
});
});
}
/// Insert a table in the document
Future<void> insertTableInDocument(WidgetTester tester) async {
// open the actions menu and insert the outline block
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_table.tr(),
);
await tester.pumpAndSettle();
extension on WidgetTester {
/// Insert a table in the document
Future<void> insertTableInDocument() async {
// open the actions menu and insert the outline block
await editor.showSlashMenu();
await editor.tapSlashMenuItemWithName(
LocaleKeys.document_slashMenu_name_table.tr(),
);
await pumpAndSettle();
}
Future<void> clickMoreActionItemInTableMenu({
required SimpleTableMoreActionType type,
required int index,
required SimpleTableMoreAction action,
}) async {
if (type == SimpleTableMoreActionType.row) {
final row = find.byWidgetPredicate((w) {
return w is SimpleTableRowBlockWidget && w.node.rowIndex == index;
});
await hoverOnWidget(
row,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.row &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
} else if (type == SimpleTableMoreActionType.column) {
final column = find.byWidgetPredicate((w) {
return w is SimpleTableCellBlockWidget && w.node.columnIndex == index;
}).first;
await hoverOnWidget(
column,
onHover: () async {
final moreActionButton = find.byWidgetPredicate((w) {
return w is SimpleTableMoreActionMenu &&
w.type == SimpleTableMoreActionType.column &&
w.index == index;
});
await tapButton(moreActionButton);
await tapButton(find.text(action.name));
},
);
await pumpAndSettle();
}
await tapAt(Offset.zero);
}
}

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
@ -87,7 +88,7 @@ void main() {
);
expect(
importedPageEditorState.getNodeAtPath([2])!.type,
TableBlockKeys.type,
SimpleTableBlockKeys.type,
);
});
});

View File

@ -5,10 +5,6 @@ import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/mobile_block_action_buttons.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_copy_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
@ -147,6 +143,8 @@ void _customBlockOptionActions(
level > 0) {
final offset = [14.0, 11.0, 8.0, 6.0, 4.0, 2.0];
top += offset[level - 1];
} else if (type == SimpleTableBlockKeys.type) {
top += 8.0;
} else {
top += 2.0;
}

View File

@ -34,7 +34,6 @@ class BlockActionList extends StatelessWidget {
editorState: editorState,
showSlashMenu: showSlashMenu,
),
const HSpace(4.0),
BlockOptionButton(
blockComponentContext: blockComponentContext,
blockComponentState: blockComponentState,
@ -42,7 +41,7 @@ class BlockActionList extends StatelessWidget {
editorState: editorState,
blockComponentBuilder: blockComponentBuilder,
),
const HSpace(4.0),
const HSpace(8.0),
],
);
}

View File

@ -86,6 +86,11 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
Log.error('Block type $type is not valid');
if (node.type == TableBlockKeys.type) {
copiedNode = _fixTableBlock(node);
copiedNode = _convertTableToSimpleTable(copiedNode);
}
} else {
if (node.type == TableBlockKeys.type) {
copiedNode = _convertTableToSimpleTable(node);
}
}
}
@ -138,6 +143,44 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
);
}
Node _convertTableToSimpleTable(Node node) {
if (node.type != TableBlockKeys.type) {
return node;
}
// the table node should contains colsLen and rowsLen
final colsLen = node.attributes[TableBlockKeys.colsLen];
final rowsLen = node.attributes[TableBlockKeys.rowsLen];
if (colsLen == null || rowsLen == null) {
return node;
}
final rows = <List<Node>>[];
final children = node.children;
for (var i = 0; i < rowsLen; i++) {
final row = <Node>[];
for (var j = 0; j < colsLen; j++) {
final cell = children
.where(
(n) =>
n.attributes[TableCellBlockKeys.rowPosition] == i &&
n.attributes[TableCellBlockKeys.colPosition] == j,
)
.firstOrNull;
row.add(
simpleTableCellBlockNode(
children: [cell?.children.first.copyWith() ?? paragraphNode()],
),
);
}
rows.add(row);
}
return simpleTableBlockNode(
children: rows.map((e) => simpleTableRowBlockNode(children: e)).toList(),
);
}
Future<void> _copyLinkToBlock(Node node) async {
final context = editorState.document.root.context;
final viewId = context?.read<DocumentBloc>().documentId;
@ -447,7 +490,7 @@ class BlockActionOptionCubit extends Cubit<BlockActionOptionState> {
// We move views after applying transaction to avoid performing side-effects on the views
final viewIdsToMove = _extractChildViewIds(selectedNodes);
for (final viewId in viewIdsToMove) {
// Attempt to put back from trash if neccessary
// Attempt to put back from trash if necessary
await TrashService.putback(viewId);
await ViewBackendService.moveViewV2(

View File

@ -2,7 +2,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -111,9 +110,7 @@ class _OptionButtonState extends State<OptionButton> {
widget.blockComponentContext.node,
beforeSelection,
);
Log.info(
'update block selection, beforeSelection: $beforeSelection, afterSelection: $selection',
);
widget.editorState.updateSelectionWithReason(
selection,
customSelectionType: SelectionType.block,

View File

@ -1,11 +1,11 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
/// Copy.
///
@ -59,11 +59,12 @@ KeyEventResult handleCopyCommand(
// plain text.
text = editorState.getTextInSelection(selection).join('\n');
final selectedNodes = editorState.getSelectedNodes(selection: selection);
final nodes = _handleSubPageNodes(selectedNodes, isCut);
final document = Document.blank()..insert([0], nodes);
final document = _buildCopiedDocument(
editorState,
selection,
isCut: isCut,
);
// in app json
inAppJson = jsonEncode(document.toJson());
// html
@ -83,6 +84,34 @@ KeyEventResult handleCopyCommand(
return KeyEventResult.handled;
}
Document _buildCopiedDocument(
EditorState editorState,
Selection selection, {
bool isCut = false,
}) {
// filter the table nodes
final filteredNodes = <Node>[];
final selectedNodes = editorState.getSelectedNodes(selection: selection);
final nodes = _handleSubPageNodes(selectedNodes, isCut);
for (final node in nodes) {
if (node.type == SimpleTableCellBlockKeys.type) {
// if the node is a table cell, we will fetch its children instead.
filteredNodes.addAll(node.children);
} else if (node.type == SimpleTableRowBlockKeys.type) {
// if the node is a table row, we will fetch its children instead.
filteredNodes.addAll(node.children.expand((e) => e.children));
} else {
filteredNodes.add(node);
}
}
final document = Document.blank()
..insert(
[0],
filteredNodes.map((e) => e.copyWith()),
);
return document;
}
List<Node> _handleSubPageNodes(List<Node> nodes, [bool isCut = false]) {
final handled = <Node>[];
for (final node in nodes) {

View File

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/clipboard_state.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// cut.
@ -42,6 +41,11 @@ CommandShortcutEventHandler _cutCommandHandler = (editorState) {
if (node == null) {
return KeyEventResult.handled;
}
// prevent to cut the node that is selecting the table.
if (node.parentTableNode != null) {
return KeyEventResult.skipRemainingHandlers;
}
final transaction = editorState.transaction;
transaction.deleteNode(node);
final nextNode = node.next;

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/services.dart';

View File

@ -1,3 +1,4 @@
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
extension AskAINodeExtension on EditorState {
@ -32,7 +33,7 @@ extension AskAINodeExtension on EditorState {
slicedNodes.add(copiedNode);
}
final markdown = documentToMarkdown(
final markdown = customDocumentToMarkdown(
Document.blank()..insert([0], slicedNodes),
);

View File

@ -1,4 +1,6 @@
export 'callout_node_parser.dart';
export 'custom_image_node_parser.dart';
export 'markdown_code_parser.dart';
export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart';
export 'simple_table_parser.dart';

View File

@ -0,0 +1,103 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:markdown/markdown.dart' as md;
class MarkdownSimpleTableParser extends CustomMarkdownParser {
const MarkdownSimpleTableParser();
@override
List<Node> transform(
md.Node element,
List<CustomMarkdownParser> parsers, {
MarkdownListType listType = MarkdownListType.unknown,
int? startNumber,
}) {
if (element is! md.Element) {
return [];
}
if (element.tag != 'table') {
return [];
}
final ec = element.children;
if (ec == null || ec.isEmpty) {
return [];
}
final th = ec
.whereType<md.Element>()
.where((e) => e.tag == 'thead')
.firstOrNull
?.children
?.whereType<md.Element>()
.where((e) => e.tag == 'tr')
.expand((e) => e.children?.whereType<md.Element>().toList() ?? [])
.where((e) => e.tag == 'th')
.toList();
final tr = ec
.whereType<md.Element>()
.where((e) => e.tag == 'tbody')
.firstOrNull
?.children
?.whereType<md.Element>()
.where((e) => e.tag == 'tr')
.toList();
if (th == null || tr == null || th.isEmpty || tr.isEmpty) {
return [];
}
final rows = <Node>[];
// Add header cells
rows.add(
simpleTableRowBlockNode(
children: th
.map(
(e) => simpleTableCellBlockNode(
children: [
paragraphNode(
delta: DeltaMarkdownDecoder().convertNodes(e.children),
),
],
),
)
.toList(),
),
);
// Add body cells
for (var i = 0; i < tr.length; i++) {
final td = tr[i]
.children
?.whereType<md.Element>()
.where((e) => e.tag == 'td')
.toList();
if (td == null || td.isEmpty) {
continue;
}
rows.add(
simpleTableRowBlockNode(
children: td
.map(
(e) => simpleTableCellBlockNode(
children: [
paragraphNode(
delta: DeltaMarkdownDecoder().convertNodes(e.children),
),
],
),
)
.toList(),
),
);
}
return [simpleTableBlockNode(children: rows)];
}
}

View File

@ -0,0 +1,86 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
/// Parser for converting SimpleTable nodes to markdown format
class SimpleTableNodeParser extends NodeParser {
const SimpleTableNodeParser();
@override
String get id => SimpleTableBlockKeys.type;
@override
String transform(Node node, DocumentMarkdownEncoder? encoder) {
try {
final tableData = _extractTableData(node);
if (tableData.isEmpty) {
return '';
}
return _buildMarkdownTable(tableData);
} catch (e) {
return '';
}
}
/// Extracts table data from the node structure into a 2D list of strings
/// Each inner list represents a row, and each string represents a cell's content
List<List<String>> _extractTableData(Node node) {
final tableData = <List<String>>[];
final rows = node.children;
for (final row in rows) {
final rowData = _extractRowData(row);
tableData.add(rowData);
}
return tableData;
}
/// Extracts data from a single table row
List<String> _extractRowData(Node row) {
final rowData = <String>[];
final cells = row.children;
for (final cell in cells) {
final content = _extractCellContent(cell);
rowData.add(content);
}
return rowData;
}
/// Extracts and formats content from a single table cell
String _extractCellContent(Node cell) {
final contentBuffer = StringBuffer();
for (final child in cell.children) {
final delta = child.delta;
if (delta == null) continue;
final content = DeltaMarkdownEncoder().convert(delta);
// Escape pipe characters to prevent breaking markdown table structure
contentBuffer.write(content.replaceAll('|', '\\|'));
}
return contentBuffer.toString();
}
/// Builds a markdown table string from the extracted table data
/// First row is treated as header, followed by separator row and data rows
String _buildMarkdownTable(List<List<String>> tableData) {
final markdown = StringBuffer();
final columnCount = tableData[0].length;
// Add header row
markdown.writeln('|${tableData[0].join('|')}|');
// Add separator row
markdown.writeln('|${List.filled(columnCount, '---').join('|')}|');
// Add data rows (skip header row)
for (int i = 1; i < tableData.length; i++) {
markdown.writeln('|${tableData[i].join('|')}|');
}
return markdown.toString();
}
}

View File

@ -58,9 +58,11 @@ export 'openai/widgets/ask_ai_block_component.dart';
export 'openai/widgets/ask_ai_toolbar_item.dart';
export 'outline/outline_block_component.dart';
export 'parsers/markdown_parsers.dart';
export 'parsers/markdown_simple_table_parser.dart';
export 'quote/quote_block_shortcuts.dart';
export 'shortcuts/character_shortcuts.dart';
export 'shortcuts/command_shortcuts.dart';
export 'simple_table/simple_table.dart';
export 'slash_menu/slash_menu_items.dart';
export 'sub_page/sub_page_block_component.dart';
export 'table/table_menu.dart';

View File

@ -1,7 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_commands.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/undo_redo/custom_undo_redo_commands.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,10 +1,8 @@
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';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_more_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
@ -12,6 +10,10 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// Only displaying the add row / add column / add column and row button
/// when hovering on the last row / last column / last cell.
bool _enableHoveringLogicV2 = true;
class SimpleTableReorderButton extends StatelessWidget {
const SimpleTableReorderButton({
super.key,
@ -53,45 +55,99 @@ class SimpleTableReorderButton extends StatelessWidget {
}
}
class SimpleTableAddRowHoverButton extends StatelessWidget {
class SimpleTableAddRowHoverButton extends StatefulWidget {
const SimpleTableAddRowHoverButton({
super.key,
required this.editorState,
required this.node,
required this.tableNode,
});
final EditorState editorState;
final Node node;
final Node tableNode;
@override
State<SimpleTableAddRowHoverButton> createState() =>
_SimpleTableAddRowHoverButtonState();
}
class _SimpleTableAddRowHoverButtonState
extends State<SimpleTableAddRowHoverButton> {
late final interceptorKey =
'simple_table_add_row_hover_button_${widget.tableNode.id}';
SelectionGestureInterceptor? interceptor;
@override
void initState() {
super.initState();
interceptor = SelectionGestureInterceptor(
key: interceptorKey,
canTap: (details) => !_isTapInBounds(details.globalPosition),
);
widget.editorState.service.selectionService
.registerGestureInterceptor(interceptor!);
}
@override
void dispose() {
widget.editorState.service.selectionService.unregisterGestureInterceptor(
interceptorKey,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(node.type == SimpleTableBlockKeys.type);
assert(widget.tableNode.type == SimpleTableBlockKeys.type);
if (node.type != SimpleTableBlockKeys.type) {
if (widget.tableNode.type != SimpleTableBlockKeys.type) {
return const SizedBox.shrink();
}
final simpleTableContext = context.read<SimpleTableContext>();
return ValueListenableBuilder(
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
builder: (context, tableCell, child) {
if (tableCell == null) {
return const SizedBox.shrink();
}
final showRowButton = tableCell.rowIndex + 1 == tableCell.rowLength;
return showRowButton
? Positioned(
bottom: 0,
left: SimpleTableConstants.tableLeftPadding -
SimpleTableConstants.cellBorderWidth,
right: SimpleTableConstants.addRowButtonRightPadding,
child: SimpleTableAddRowButton(
onTap: () => editorState.addRowInTable(node),
),
)
: const SizedBox.shrink();
valueListenable: simpleTableContext.isHoveringOnTableBlock,
builder: (context, isHoveringOnTableBlock, child) {
return ValueListenableBuilder(
valueListenable: simpleTableContext.hoveringTableCell,
builder: (context, hoveringTableCell, child) {
bool shouldShow = isHoveringOnTableBlock;
if (hoveringTableCell != null && _enableHoveringLogicV2) {
shouldShow =
hoveringTableCell.rowIndex + 1 == hoveringTableCell.rowLength;
}
return shouldShow
? Positioned(
bottom: 2 * SimpleTableConstants.addRowButtonPadding,
left: SimpleTableConstants.tableLeftPadding -
SimpleTableConstants.cellBorderWidth,
right: SimpleTableConstants.addRowButtonRightPadding,
child: SimpleTableAddRowButton(
onTap: () => widget.editorState.addRowInTable(
widget.tableNode,
),
),
)
: const SizedBox.shrink();
},
);
},
);
}
bool _isTapInBounds(Offset offset) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) {
return false;
}
final localPosition = renderBox.globalToLocal(offset);
final result = renderBox.paintBounds.contains(localPosition);
return result;
}
}
class SimpleTableAddRowButton extends StatelessWidget {
@ -107,14 +163,14 @@ class SimpleTableAddRowButton extends StatelessWidget {
return FlowyTooltip(
message: LocaleKeys.document_plugins_simpleTable_clickToAddNewRow.tr(),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
height: SimpleTableConstants.addRowButtonHeight,
margin: const EdgeInsets.symmetric(
vertical: SimpleTableConstants.addRowButtonPadding,
vertical: SimpleTableConstants.addColumnButtonPadding,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
@ -132,7 +188,7 @@ class SimpleTableAddRowButton extends StatelessWidget {
}
}
class SimpleTableAddColumnHoverButton extends StatelessWidget {
class SimpleTableAddColumnHoverButton extends StatefulWidget {
const SimpleTableAddColumnHoverButton({
super.key,
required this.editorState,
@ -143,37 +199,88 @@ class SimpleTableAddColumnHoverButton extends StatelessWidget {
final Node node;
@override
Widget build(BuildContext context) {
assert(node.type == SimpleTableBlockKeys.type);
State<SimpleTableAddColumnHoverButton> createState() =>
_SimpleTableAddColumnHoverButtonState();
}
if (node.type != SimpleTableBlockKeys.type) {
class _SimpleTableAddColumnHoverButtonState
extends State<SimpleTableAddColumnHoverButton> {
late final interceptorKey =
'simple_table_add_column_hover_button_${widget.node.id}';
SelectionGestureInterceptor? interceptor;
@override
void initState() {
super.initState();
interceptor = SelectionGestureInterceptor(
key: interceptorKey,
canTap: (details) => !_isTapInBounds(details.globalPosition),
);
widget.editorState.service.selectionService
.registerGestureInterceptor(interceptor!);
}
@override
void dispose() {
widget.editorState.service.selectionService.unregisterGestureInterceptor(
interceptorKey,
);
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(widget.node.type == SimpleTableBlockKeys.type);
if (widget.node.type != SimpleTableBlockKeys.type) {
return const SizedBox.shrink();
}
return ValueListenableBuilder(
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
builder: (context, tableCell, child) {
if (tableCell == null) {
return const SizedBox.shrink();
}
final showColumnButton =
tableCell.columnIndex + 1 == tableCell.columnLength;
return showColumnButton
? Positioned(
top: SimpleTableConstants.tableTopPadding -
SimpleTableConstants.cellBorderWidth,
bottom: SimpleTableConstants.addColumnButtonBottomPadding,
right: 0,
child: SimpleTableAddColumnButton(
onTap: () {
editorState.addColumnInTable(node);
},
),
)
: const SizedBox.shrink();
valueListenable:
context.read<SimpleTableContext>().isHoveringOnTableBlock,
builder: (context, isHoveringOnTableBlock, _) {
return ValueListenableBuilder(
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
builder: (context, hoveringTableCell, _) {
bool shouldShow = isHoveringOnTableBlock;
if (hoveringTableCell != null && _enableHoveringLogicV2) {
shouldShow = hoveringTableCell.columnIndex + 1 ==
hoveringTableCell.columnLength;
}
return shouldShow
? Positioned(
top: SimpleTableConstants.tableTopPadding -
SimpleTableConstants.cellBorderWidth,
bottom: SimpleTableConstants.addColumnButtonBottomPadding,
right: 0,
child: SimpleTableAddColumnButton(
onTap: () {
widget.editorState.addColumnInTable(widget.node);
},
),
)
: const SizedBox.shrink();
},
);
},
);
}
bool _isTapInBounds(Offset offset) {
final renderBox = context.findRenderObject() as RenderBox?;
if (renderBox == null) {
return false;
}
final localPosition = renderBox.globalToLocal(offset);
final result = renderBox.paintBounds.contains(localPosition);
return result;
}
}
class SimpleTableAddColumnButton extends StatelessWidget {
@ -233,23 +340,28 @@ class SimpleTableAddColumnAndRowHoverButton extends StatelessWidget {
}
return ValueListenableBuilder(
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
builder: (context, tableCell, child) {
if (tableCell == null) {
return const SizedBox.shrink();
}
final showAddColumnAndRowButton =
tableCell.rowIndex + 1 == tableCell.rowLength ||
tableCell.columnIndex + 1 == tableCell.columnLength;
return showAddColumnAndRowButton
? Positioned(
bottom: SimpleTableConstants.addRowButtonPadding,
right: SimpleTableConstants.addColumnButtonPadding,
child: SimpleTableAddColumnAndRowButton(
onTap: () => editorState.addColumnAndRowInTable(node),
),
)
: const SizedBox.shrink();
valueListenable:
context.read<SimpleTableContext>().isHoveringOnTableBlock,
builder: (context, isHoveringOnTableBlock, child) {
return ValueListenableBuilder(
valueListenable: context.read<SimpleTableContext>().hoveringTableCell,
builder: (context, hoveringTableCell, child) {
bool shouldShow = isHoveringOnTableBlock;
if (hoveringTableCell != null && _enableHoveringLogicV2) {
shouldShow = hoveringTableCell.isLastCellInTable;
}
return shouldShow
? Positioned(
bottom:
SimpleTableConstants.addColumnAndRowButtonBottomPadding,
right: SimpleTableConstants.addColumnButtonPadding,
child: SimpleTableAddColumnAndRowButton(
onTap: () => editorState.addColumnAndRowInTable(node),
),
)
: const SizedBox.shrink();
},
);
},
);
}
@ -335,9 +447,6 @@ class SimpleTableAlignMenu extends StatefulWidget {
}
class _SimpleTableAlignMenuState extends State<SimpleTableAlignMenu> {
final PopoverController controller = PopoverController();
bool isOpen = false;
@override
Widget build(BuildContext context) {
final align = switch (widget.type) {
@ -345,34 +454,31 @@ class _SimpleTableAlignMenuState extends State<SimpleTableAlignMenu> {
SimpleTableMoreActionType.row => widget.tableCellNode.rowAlign,
};
return AppFlowyPopover(
controller: controller,
asBarrier: true,
mutex: widget.mutex,
child: SimpleTableBasicButton(
leftIconSvg: align.leftIconSvg,
text: LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
onTap: () {
if (!isOpen) {
controller.show();
}
},
onTap: () {},
),
onClose: () => isOpen = false,
popupBuilder: (_) {
isOpen = true;
popupBuilder: (popoverContext) {
void onClose() => PopoverContainer.of(popoverContext).closeAll();
return Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildAlignButton(context, TableAlign.left),
_buildAlignButton(context, TableAlign.center),
_buildAlignButton(context, TableAlign.right),
_buildAlignButton(context, TableAlign.left, onClose),
_buildAlignButton(context, TableAlign.center, onClose),
_buildAlignButton(context, TableAlign.right, onClose),
],
);
},
);
}
Widget _buildAlignButton(BuildContext context, TableAlign align) {
Widget _buildAlignButton(
BuildContext context,
TableAlign align,
VoidCallback onClose,
) {
return SimpleTableBasicButton(
leftIconSvg: align.leftIconSvg,
text: align.name,
@ -392,7 +498,7 @@ class _SimpleTableAlignMenuState extends State<SimpleTableAlignMenu> {
break;
}
PopoverContainer.of(context).close();
onClose();
},
);
}
@ -481,15 +587,25 @@ class _SimpleTableColumnResizeHandleState
},
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;
@ -543,11 +659,9 @@ class SimpleTableBackgroundColorMenu extends StatefulWidget {
class _SimpleTableBackgroundColorMenuState
extends State<SimpleTableBackgroundColorMenu> {
final PopoverController controller = PopoverController();
bool isOpen = false;
@override
Widget build(BuildContext context) {
final theme = AFThemeExtension.of(context);
final backgroundColor = switch (widget.type) {
SimpleTableMoreActionType.row =>
widget.tableCellNode.buildRowColor(context),
@ -555,39 +669,30 @@ class _SimpleTableBackgroundColorMenuState
widget.tableCellNode.buildColumnColor(context),
};
return AppFlowyPopover(
controller: controller,
mutex: widget.mutex,
asBarrier: true,
popupBuilder: (_) {
isOpen = true;
popupBuilder: (popoverContext) {
return _buildColorOptionMenu(
context,
controller,
theme: theme,
onClose: () => PopoverContainer.of(popoverContext).closeAll(),
);
},
onClose: () => isOpen = false,
direction: PopoverDirection.rightWithCenterAligned,
animationDuration: Durations.short3,
beginScaleFactor: 1.0,
beginOpacity: 0.8,
child: SimpleTableBasicButton(
leftIconBuilder: (onHover) => ColorOptionIcon(
color: backgroundColor ?? Colors.transparent,
),
text: LocaleKeys.document_plugins_simpleTable_moreActions_color.tr(),
onTap: () {
if (!isOpen) {
controller.show();
}
},
onTap: () {},
),
);
}
Widget _buildColorOptionMenu(
BuildContext context,
PopoverController controller,
) {
BuildContext context, {
required AFThemeExtension theme,
required VoidCallback onClose,
}) {
final colors = [
// reset to default background color
FlowyColorOption(
@ -597,7 +702,7 @@ class _SimpleTableBackgroundColorMenuState
),
...FlowyTint.values.map(
(e) => FlowyColorOption(
color: e.color(context),
color: e.color(context, theme: theme),
i18n: e.tintName(AppFlowyEditorL10n.current),
id: e.id,
),
@ -607,7 +712,7 @@ class _SimpleTableBackgroundColorMenuState
return FlowyColorPicker(
colors: colors,
border: Border.all(
color: AFThemeExtension.of(context).onBackground,
color: theme.onBackground,
),
onTap: (option, index) {
switch (widget.type) {
@ -625,8 +730,7 @@ class _SimpleTableBackgroundColorMenuState
break;
}
controller.close();
PopoverContainer.of(context).close();
onClose();
},
);
}

View File

@ -0,0 +1,7 @@
export 'simple_table_block_component.dart';
export 'simple_table_cell_block_component.dart';
export 'simple_table_constants.dart';
export 'simple_table_more_action.dart';
export 'simple_table_operations/simple_table_operations.dart';
export 'simple_table_row_block_component.dart';
export 'simple_table_shortcuts/simple_table_commands.dart';

View File

@ -1,7 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart';
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_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -92,12 +92,17 @@ Node createSimpleTableBlockNode({
required int columnCount,
required int rowCount,
String? defaultContent,
String Function(int rowIndex, int columnIndex)? contentBuilder,
}) {
final rows = List.generate(rowCount, (_) {
final rows = List.generate(rowCount, (rowIndex) {
final cells = List.generate(
columnCount,
(_) => simpleTableCellBlockNode(
children: [paragraphNode(text: defaultContent)],
(columnIndex) => simpleTableCellBlockNode(
children: [
paragraphNode(
text: defaultContent ?? contentBuilder?.call(rowIndex, columnIndex),
),
],
),
);
return simpleTableRowBlockNode(children: cells);
@ -203,37 +208,39 @@ class _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>
);
}
return child;
}
Widget _buildTable() {
const bottomPadding = SimpleTableConstants.addRowButtonHeight +
2 * SimpleTableConstants.addRowButtonPadding;
const rightPadding = SimpleTableConstants.addColumnButtonWidth +
2 * SimpleTableConstants.addColumnButtonPadding;
// IntrinsicWidth and IntrinsicHeight are used to make the table size fit the content.
return Provider.value(
value: simpleTableContext,
child: MouseRegion(
onEnter: (event) => simpleTableContext.isHoveringOnTable.value = true,
onEnter: (event) =>
simpleTableContext.isHoveringOnTableBlock.value = true,
onExit: (event) {
simpleTableContext.isHoveringOnTable.value = false;
simpleTableContext.hoveringTableCell.value = null;
simpleTableContext.isHoveringOnTableBlock.value = false;
},
child: Stack(
children: [
Scrollbar(
child: child,
),
);
}
Widget _buildTable() {
// IntrinsicWidth and IntrinsicHeight are used to make the table size fit the content.
return Provider.value(
value: simpleTableContext,
child: Stack(
children: [
MouseRegion(
onEnter: (event) =>
simpleTableContext.isHoveringOnTable.value = true,
onExit: (event) {
simpleTableContext.isHoveringOnTable.value = false;
simpleTableContext.hoveringTableCell.value = null;
},
child: Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.only(
top: SimpleTableConstants.tableTopPadding,
left: SimpleTableConstants.tableLeftPadding,
bottom: bottomPadding,
right: rightPadding,
),
padding: SimpleTableConstants.tablePadding,
child: IntrinsicWidth(
child: IntrinsicHeight(
child: Column(
@ -246,20 +253,20 @@ class _SimpleTableBlockWidgetState extends State<SimpleTableBlockWidget>
),
),
),
SimpleTableAddColumnHoverButton(
editorState: editorState,
node: node,
),
SimpleTableAddRowHoverButton(
editorState: editorState,
node: node,
),
SimpleTableAddColumnAndRowHoverButton(
editorState: editorState,
node: node,
),
],
),
),
SimpleTableAddColumnHoverButton(
editorState: editorState,
node: node,
),
SimpleTableAddRowHoverButton(
editorState: editorState,
tableNode: node,
),
SimpleTableAddColumnAndRowHoverButton(
editorState: editorState,
node: node,
),
],
),
);
}

View File

@ -1,7 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_more_action.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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';
@ -61,10 +59,11 @@ class SimpleTableCellBlockWidget extends BlockComponentStatefulWidget {
@override
State<SimpleTableCellBlockWidget> createState() =>
_SimpleTableCellBlockWidgetState();
SimpleTableCellBlockWidgetState();
}
class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
@visibleForTesting
class SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
with
BlockComponentConfigurable,
BlockComponentTextDirectionMixin,
@ -78,32 +77,45 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
@override
late EditorState editorState = context.read<EditorState>();
late SimpleTableContext simpleTableContext =
context.read<SimpleTableContext>();
late SimpleTableContext? simpleTableContext =
context.read<SimpleTableContext?>();
ValueNotifier<bool> isEditingCellNotifier = ValueNotifier(false);
@override
void initState() {
super.initState();
simpleTableContext.isSelectingTable.addListener(_onSelectingTableChanged);
simpleTableContext?.isSelectingTable.addListener(_onSelectingTableChanged);
node.parentTableNode?.addListener(_onSelectingTableChanged);
editorState.selectionNotifier.addListener(_onSelectionChanged);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_onSelectionChanged();
});
}
@override
void dispose() {
simpleTableContext.isSelectingTable.removeListener(
simpleTableContext?.isSelectingTable.removeListener(
_onSelectingTableChanged,
);
node.parentTableNode?.removeListener(_onSelectingTableChanged);
editorState.selectionNotifier.removeListener(_onSelectionChanged);
isEditingCellNotifier.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (simpleTableContext == null) {
return const SizedBox.shrink();
}
return MouseRegion(
hitTestBehavior: HitTestBehavior.opaque,
onEnter: (event) => simpleTableContext.hoveringTableCell.value = node,
onEnter: (event) => simpleTableContext!.hoveringTableCell.value = node,
child: Stack(
clipBehavior: Clip.none,
children: [
@ -117,12 +129,11 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
Positioned(
left: 0,
right: 0,
top: -SimpleTableConstants.tableTopPadding,
child: _buildColumnMoreActionButton(),
),
Positioned(
right: 0,
top: 0,
top: node.rowIndex == 0 ? SimpleTableConstants.tableTopPadding : 0,
bottom: 0,
child: SimpleTableColumnResizeHandle(
node: node,
@ -134,21 +145,38 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
}
Widget _buildCell() {
return ValueListenableBuilder(
valueListenable: simpleTableContext.selectingColumn,
builder: (context, selectingColumn, child) {
return ValueListenableBuilder(
valueListenable: simpleTableContext.selectingRow,
builder: (context, selectingRow, _) {
return DecoratedBox(
decoration: _buildDecoration(),
child: child!,
);
},
);
},
child: Column(
children: node.children.map(_buildCellContent).toList(),
if (simpleTableContext == null) {
return const SizedBox.shrink();
}
return Padding(
// add padding to the top of the cell if it is the first row, otherwise the
// column action button is not clickable.
// issue: https://github.com/flutter/flutter/issues/75747
padding: EdgeInsets.only(
top: node.rowIndex == 0 ? SimpleTableConstants.tableTopPadding : 0,
),
child: ValueListenableBuilder<bool>(
valueListenable: isEditingCellNotifier,
builder: (context, isEditingCell, child) {
return ValueListenableBuilder(
valueListenable: simpleTableContext!.selectingColumn,
builder: (context, selectingColumn, child) {
return ValueListenableBuilder(
valueListenable: simpleTableContext!.selectingRow,
builder: (context, selectingRow, _) {
return DecoratedBox(
decoration: _buildDecoration(),
child: child!,
);
},
);
},
child: Column(
children: node.children.map(_buildCellContent).toList(),
),
);
},
),
);
}
@ -257,6 +285,8 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
return _buildColumnBorder();
} else if (isCellInSelectedRow) {
return _buildRowBorder();
} else if (isEditingCellNotifier.value) {
return _buildEditingBorder();
} else {
return _buildCellBorder();
}
@ -283,28 +313,14 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
/// the border wrapping the cell 2 and cell 4 is the column border
Border _buildColumnBorder() {
return Border(
left: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
right: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.5,
),
left: _buildHighlightBorderSide(),
right: _buildHighlightBorderSide(),
top: node.rowIndex == 0
? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: BorderSide(
color: context.simpleTableBorderColor,
),
? _buildHighlightBorderSide()
: _buildDefaultBorderSide(),
bottom: node.rowIndex + 1 == node.parentTableNode?.rowLength
? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: BorderSide.none,
? _buildHighlightBorderSide()
: _buildDefaultBorderSide(),
);
}
@ -318,35 +334,38 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
/// the border wrapping the cell 1 and cell 2 is the row border
Border _buildRowBorder() {
return Border(
top: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
),
bottom: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2.5,
),
top: _buildHighlightBorderSide(),
bottom: _buildHighlightBorderSide(),
left: node.columnIndex == 0
? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: BorderSide(
color: context.simpleTableBorderColor,
),
? _buildHighlightBorderSide()
: _buildDefaultBorderSide(),
right: node.columnIndex + 1 == node.parentTableNode?.columnLength
? BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 2,
)
: BorderSide.none,
? _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: context.simpleTableBorderColor,
strokeAlign: BorderSide.strokeAlignCenter,
color: Theme.of(context).colorScheme.primary,
width: 2,
);
}
@ -355,37 +374,37 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
final columnIndex = node.columnIndex;
return Border(
top: rowIndex == 0
? _buildDefaultBorderSide()
: BorderSide(
color: context.simpleTableBorderColor,
width: 0.5,
),
top:
rowIndex == 0 ? _buildHighlightBorderSide() : _buildLightBorderSide(),
bottom: rowIndex + 1 == node.parentTableNode?.rowLength
? _buildDefaultBorderSide()
: BorderSide(
color: context.simpleTableBorderColor,
width: 0.5,
),
? _buildHighlightBorderSide()
: _buildLightBorderSide(),
left: columnIndex == 0
? _buildDefaultBorderSide()
: BorderSide(
color: context.simpleTableBorderColor,
width: 0.5,
),
? _buildHighlightBorderSide()
: _buildLightBorderSide(),
right: columnIndex + 1 == node.parentTableNode?.columnLength
? _buildDefaultBorderSide()
: BorderSide(
color: context.simpleTableBorderColor,
width: 0.5,
),
? _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: Theme.of(context).colorScheme.primary,
width: 2,
color: context.simpleTableBorderColor,
);
}
@ -394,4 +413,17 @@ class _SimpleTableCellBlockWidgetState extends State<SimpleTableCellBlockWidget>
setState(() {});
}
}
void _onSelectionChanged() {
final selection = editorState.selection;
// check if the selection is in the cell
if (selection != null &&
node.path.isAncestorOf(selection.start.path) &&
node.path.isAncestorOf(selection.end.path)) {
isEditingCellNotifier.value = true;
} else {
isEditingCellNotifier.value = false;
}
}
}

View File

@ -1,10 +1,10 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
const enableTableDebugLog = false;
const enableTableDebugLog = true;
class SimpleTableContext {
SimpleTableContext() {
@ -14,6 +14,7 @@ class SimpleTableContext {
selectingColumn.addListener(_onSelectingColumnChanged);
selectingRow.addListener(_onSelectingRowChanged);
isSelectingTable.addListener(_onSelectingTableChanged);
isHoveringOnTableBlock.addListener(_onHoveringOnTableBlockChanged);
}
}
@ -23,6 +24,7 @@ class SimpleTableContext {
final ValueNotifier<int?> selectingColumn = ValueNotifier(null);
final ValueNotifier<int?> selectingRow = ValueNotifier(null);
final ValueNotifier<bool> isSelectingTable = ValueNotifier(false);
final ValueNotifier<bool> isHoveringOnTableBlock = ValueNotifier(false);
void _onHoveringOnTableChanged() {
if (!enableTableDebugLog) {
@ -69,6 +71,14 @@ class SimpleTableContext {
Log.debug('isSelectingTable: ${isSelectingTable.value}');
}
void _onHoveringOnTableBlockChanged() {
if (!enableTableDebugLog) {
return;
}
Log.debug('isHoveringOnTableBlock: ${isHoveringOnTableBlock.value}');
}
void dispose() {
isHoveringOnTable.dispose();
hoveringTableCell.dispose();
@ -76,6 +86,7 @@ class SimpleTableContext {
selectingColumn.dispose();
selectingRow.dispose();
isSelectingTable.dispose();
isHoveringOnTableBlock.dispose();
}
}
@ -87,9 +98,22 @@ class SimpleTableConstants {
static const tableTopPadding = 8.0;
static const tableLeftPadding = 8.0;
static const tableBottomPadding =
addRowButtonHeight + 3 * addRowButtonPadding;
static const tableRightPadding =
addColumnButtonWidth + 2 * SimpleTableConstants.addColumnButtonPadding;
static const tablePadding = EdgeInsets.only(
// don't add padding to the top of the table, the first row will have padding
// to make the column action button clickable.
bottom: tableBottomPadding,
left: tableLeftPadding,
right: tableRightPadding,
);
// Add row button
static const addRowButtonHeight = 16.0;
static const addRowButtonPadding = 2.0;
static const addRowButtonPadding = 4.0;
static const addRowButtonRadius = 4.0;
static const addRowButtonRightPadding =
addColumnButtonWidth + addColumnButtonPadding * 2;
@ -99,12 +123,13 @@ class SimpleTableConstants {
static const addColumnButtonPadding = 2.0;
static const addColumnButtonRadius = 4.0;
static const addColumnButtonBottomPadding =
addRowButtonHeight + addRowButtonPadding * 2;
addRowButtonHeight + 3 * addRowButtonPadding;
// Add column and row button
static const addColumnAndRowButtonWidth = addColumnButtonWidth;
static const addColumnAndRowButtonHeight = addRowButtonHeight;
static const addColumnAndRowButtonCornerRadius = addColumnButtonWidth / 2.0;
static const addColumnAndRowButtonBottomPadding = 2.5 * addRowButtonPadding;
// Table cell
static const cellEdgePadding = EdgeInsets.symmetric(

View File

@ -1,9 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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_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/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
@ -166,6 +166,7 @@ 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,
@ -242,6 +243,13 @@ class _SimpleTableMoreActionPopupState
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;
@ -297,17 +305,28 @@ class _SimpleTableMoreActionPopupState
}
}
class SimpleTableMoreActionList extends StatelessWidget {
class SimpleTableMoreActionList extends StatefulWidget {
const SimpleTableMoreActionList({
super.key,
required this.type,
required this.index,
required this.tableCellNode,
this.mutex,
});
final SimpleTableMoreActionType type;
final int index;
final Node tableCellNode;
final PopoverMutex? mutex;
@override
State<SimpleTableMoreActionList> createState() =>
_SimpleTableMoreActionListState();
}
class _SimpleTableMoreActionListState extends State<SimpleTableMoreActionList> {
// ensure the background color menu and align menu exclusive
final mutex = PopoverMutex();
@override
Widget build(BuildContext context) {
@ -316,9 +335,10 @@ class SimpleTableMoreActionList extends StatelessWidget {
children: _buildActions()
.map(
(action) => SimpleTableMoreActionItem(
type: type,
type: widget.type,
action: action,
tableCellNode: tableCellNode,
tableCellNode: widget.tableCellNode,
popoverMutex: mutex,
),
)
.toList(),
@ -326,26 +346,27 @@ class SimpleTableMoreActionList extends StatelessWidget {
}
List<SimpleTableMoreAction> _buildActions() {
final actions = type.actions;
final actions = widget.type.actions;
// if the index is 0, add the divider and enable header action
if (index == 0) {
if (widget.index == 0) {
actions.addAll([
SimpleTableMoreAction.divider,
if (type == SimpleTableMoreActionType.column)
if (widget.type == SimpleTableMoreActionType.column)
SimpleTableMoreAction.enableHeaderColumn,
if (type == SimpleTableMoreActionType.row)
if (widget.type == SimpleTableMoreActionType.row)
SimpleTableMoreAction.enableHeaderRow,
]);
}
// if the table only contains one row or one column, remove the delete action
if (tableCellNode.rowLength == 1 && type == SimpleTableMoreActionType.row) {
if (widget.tableCellNode.rowLength == 1 &&
widget.type == SimpleTableMoreActionType.row) {
actions.remove(SimpleTableMoreAction.delete);
}
if (tableCellNode.columnLength == 1 &&
type == SimpleTableMoreActionType.column) {
if (widget.tableCellNode.columnLength == 1 &&
widget.type == SimpleTableMoreActionType.column) {
actions.remove(SimpleTableMoreAction.delete);
}
@ -359,11 +380,13 @@ class SimpleTableMoreActionItem extends StatefulWidget {
required this.type,
required this.action,
required this.tableCellNode,
required this.popoverMutex,
});
final SimpleTableMoreActionType type;
final SimpleTableMoreAction action;
final Node tableCellNode;
final PopoverMutex popoverMutex;
@override
State<SimpleTableMoreActionItem> createState() =>
@ -371,10 +394,7 @@ class SimpleTableMoreActionItem extends StatefulWidget {
}
class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
// ensure the background color menu and align menu exclusive
final mutex = PopoverMutex();
ValueNotifier<bool> isEnableHeader = ValueNotifier(false);
final isEnableHeader = ValueNotifier(false);
@override
void initState() {
@ -419,7 +439,7 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
return SimpleTableAlignMenu(
type: widget.type,
tableCellNode: widget.tableCellNode,
mutex: mutex,
mutex: widget.popoverMutex,
);
}
@ -427,7 +447,7 @@ class _SimpleTableMoreActionItemState extends State<SimpleTableMoreActionItem> {
return SimpleTableBackgroundColorMenu(
type: widget.type,
tableCellNode: widget.tableCellNode,
mutex: mutex,
mutex: widget.popoverMutex,
);
}

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_operations/simple_table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,6 +1,6 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_operations/simple_table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,8 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_cell_block_component.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_operations/simple_table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@ -20,15 +20,15 @@ extension TableInsertionOperations on EditorState {
/// Row 2: | | | |
/// Row 3: | | | | New row
///
Future<void> addRowInTable(Node node) async {
assert(node.type == SimpleTableBlockKeys.type);
Future<void> addRowInTable(Node tableNode) async {
assert(tableNode.type == SimpleTableBlockKeys.type);
if (node.type != SimpleTableBlockKeys.type) {
Log.warn('node is not a table node: ${node.type}');
if (tableNode.type != SimpleTableBlockKeys.type) {
Log.warn('node is not a table node: ${tableNode.type}');
return;
}
await insertRowInTable(node, node.rowLength);
await insertRowInTable(tableNode, tableNode.rowLength);
}
/// Add a column at the end of the table.

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';

View File

@ -1,9 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart';
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_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_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
@ -83,8 +83,12 @@ extension TableNodeExtension on Node {
}
int get rowIndex {
assert(type == SimpleTableCellBlockKeys.type);
return path.parent.last;
if (type == SimpleTableCellBlockKeys.type) {
return path.parent.last;
} else if (type == SimpleTableRowBlockKeys.type) {
return path.last;
}
return -1;
}
int get columnIndex {
@ -165,6 +169,8 @@ extension TableNodeExtension on Node {
tableNode = parent;
} else if (type == SimpleTableCellBlockKeys.type) {
tableNode = parent?.parent;
} else {
return parent?.parentTableNode;
}
if (tableNode == null || tableNode.type != SimpleTableBlockKeys.type) {

View File

@ -0,0 +1,8 @@
export 'simple_table_content_operation.dart';
export 'simple_table_delete_operation.dart';
export 'simple_table_duplicate_operation.dart';
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_style_operation.dart';

View File

@ -1,8 +1,8 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart';
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_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_operations/simple_table_map_operation.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:universal_platform/universal_platform.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shared_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
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_constants.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final CommandShortcutEvent arrowLeftInTableCell = CommandShortcutEvent(

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final CommandShortcutEvent arrowRightInTableCell = CommandShortcutEvent(

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.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_operations/simple_table_operations.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -18,16 +18,33 @@ extension TableCommandExtension on EditorState {
/// The third element is the node that is the current selection.
IsInTableCellResult isCurrentSelectionInTableCell() {
final selection = this.selection;
if (selection == null || !selection.isCollapsed) {
if (selection == null) {
return (false, null, null, null);
}
final node = document.nodeAtPath(selection.end.path);
final tableCellParent = node?.findParent(
(node) => node.type == SimpleTableCellBlockKeys.type,
);
final isInTableCell = tableCellParent != null;
return (isInTableCell, selection, tableCellParent, node);
if (selection.isCollapsed) {
// if the selection is collapsed, check if the node is in a table cell
final node = document.nodeAtPath(selection.end.path);
final tableCellParent = node?.findParent(
(node) => node.type == SimpleTableCellBlockKeys.type,
);
final isInTableCell = tableCellParent != null;
return (isInTableCell, selection, tableCellParent, node);
} else {
// if the selection is not collapsed, check if the start and end nodes are in a table cell
final startNode = document.nodeAtPath(selection.start.path);
final endNode = document.nodeAtPath(selection.end.path);
final startNodeInTableCell = startNode?.findParent(
(node) => node.type == SimpleTableCellBlockKeys.type,
);
final endNodeInTableCell = endNode?.findParent(
(node) => node.type == SimpleTableCellBlockKeys.type,
);
final isInSameTableCell = startNodeInTableCell != null &&
endNodeInTableCell != null &&
startNodeInTableCell.path.equals(endNodeInTableCell.path);
return (isInSameTableCell, selection, startNodeInTableCell, endNode);
}
}
/// Move the selection to the previous cell

View File

@ -0,0 +1,18 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart';
final simpleTableCommands = [
arrowUpInTableCell,
arrowDownInTableCell,
arrowLeftInTableCell,
arrowRightInTableCell,
tabInTableCell,
shiftTabInTableCell,
backspaceInTableCell,
selectAllInTableCellCommand,
];

View File

@ -0,0 +1,46 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
final CommandShortcutEvent selectAllInTableCellCommand = CommandShortcutEvent(
key: 'Select all contents in table cell',
getDescription: () => 'Select all contents in table cell',
command: 'ctrl+a',
macOSCommand: 'cmd+a',
handler: _selectAllInTableCellHandler,
);
KeyEventResult _selectAllInTableCellHandler(EditorState editorState) {
final (isInTableCell, selection, tableCellNode, _) =
editorState.isCurrentSelectionInTableCell();
if (!isInTableCell || selection == null || tableCellNode == null) {
return KeyEventResult.ignored;
}
final firstFocusableChild = tableCellNode.children.firstWhereOrNull(
(e) => e.delta != null,
);
final lastFocusableChild = tableCellNode.lastChildWhere(
(e) => e.delta != null,
);
if (firstFocusableChild == null || lastFocusableChild == null) {
return KeyEventResult.ignored;
}
final afterSelection = Selection(
start: Position(path: firstFocusableChild.path),
end: Position(
path: lastFocusableChild.path,
offset: lastFocusableChild.delta?.length ?? 0,
),
);
if (afterSelection == editorState.selection) {
// Focus on the cell already
return KeyEventResult.ignored;
} else {
editorState.selection = afterSelection;
return KeyEventResult.handled;
}
}

View File

@ -1,5 +1,5 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
final CommandShortcutEvent tabInTableCell = CommandShortcutEvent(

View File

@ -8,9 +8,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/imag
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/slash_menu_items.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/shared_context/shared_context.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
@ -599,55 +596,32 @@ SelectionMenuItem tableSlashMenuItem = SelectionMenuItem(
return;
}
final tableNode = simpleTableBlockNode(
children: [
simpleTableRowBlockNode(
children: [
simpleTableCellBlockNode(
children: [
paragraphNode(),
],
),
simpleTableCellBlockNode(
children: [
paragraphNode(),
],
),
],
),
simpleTableRowBlockNode(
children: [
simpleTableCellBlockNode(
children: [
paragraphNode(),
],
),
simpleTableCellBlockNode(
children: [
paragraphNode(),
],
),
],
),
],
// create a simple table with 2 columns and 2 rows
final tableNode = createSimpleTableBlockNode(
columnCount: 2,
rowCount: 2,
);
final transaction = editorState.transaction;
final delta = currentNode.delta;
if (delta != null && delta.isEmpty) {
final path = selection.end.path;
transaction
..insertNode(selection.end.path, tableNode)
..insertNode(path, tableNode)
..deleteNode(currentNode);
transaction.afterSelection = Selection.collapsed(
Position(
path: selection.end.path + [0, 0],
// table -> row -> cell -> paragraph
path: path + [0, 0, 0],
),
);
} else {
transaction.insertNode(selection.end.path.next, tableNode);
final path = selection.end.path.next;
transaction.insertNode(path, tableNode);
transaction.afterSelection = Selection.collapsed(
Position(
path: selection.end.path.next + [0, 0],
// table -> row -> cell -> paragraph
path: path + [0, 0, 0],
),
);
}

View File

@ -1,16 +0,0 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_down_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_left_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_right_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_up_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_backspace_command.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_tab_command.dart';
final simpleTableCommands = [
arrowUpInTableCell,
arrowDownInTableCell,
arrowLeftInTableCell,
arrowRightInTableCell,
tabInTableCell,
shiftTabInTableCell,
backspaceInTableCell,
];

View File

@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'dart:math' as math;
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_option_action.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'dart:math' as math;
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flutter/material.dart';
const tableActions = <TableOptionAction>[
TableOptionAction.addAfter,

View File

@ -1,8 +0,0 @@
export 'table_content_operation.dart';
export 'table_delete_operation.dart';
export 'table_duplicate_operation.dart';
export 'table_header_operation.dart';
export 'table_insert_operation.dart';
export 'table_map_operation.dart';
export 'table_node_extension.dart';
export 'table_style_operation.dart';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
Document customMarkdownToDocument(String markdown) {
@ -6,6 +6,20 @@ Document customMarkdownToDocument(String markdown) {
markdown,
markdownParsers: [
const MarkdownCodeBlockParser(),
const MarkdownSimpleTableParser(),
],
);
}
String customDocumentToMarkdown(Document document) {
return documentToMarkdown(
document,
customParsers: [
const MathEquationNodeParser(),
const CalloutNodeParser(),
const ToggleListNodeParser(),
const CustomImageNodeParser(),
const SimpleTableNodeParser(),
],
);
}

View File

@ -3,20 +3,13 @@ import 'dart:convert';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/prelude.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/document_markdown_parsers.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:easy_localization/easy_localization.dart';
const List<NodeParser> _customParsers = [
MathEquationNodeParser(),
CalloutNodeParser(),
ToggleListNodeParser(),
CustomImageNodeParser(),
];
enum DocumentExportType {
json,
markdown,
@ -50,10 +43,7 @@ class DocumentExporter {
case DocumentExportType.json:
return FlowyResult.success(jsonEncode(document));
case DocumentExportType.markdown:
final markdown = documentToMarkdown(
document,
customParsers: _customParsers,
);
final markdown = customDocumentToMarkdown(document);
return FlowyResult.success(markdown);
case DocumentExportType.text:
throw UnimplementedError();

View File

@ -248,16 +248,17 @@ enum FlowyTint {
return null;
}
Color color(BuildContext context) => switch (this) {
FlowyTint.tint1 => AFThemeExtension.of(context).tint1,
FlowyTint.tint2 => AFThemeExtension.of(context).tint2,
FlowyTint.tint3 => AFThemeExtension.of(context).tint3,
FlowyTint.tint4 => AFThemeExtension.of(context).tint4,
FlowyTint.tint5 => AFThemeExtension.of(context).tint5,
FlowyTint.tint6 => AFThemeExtension.of(context).tint6,
FlowyTint.tint7 => AFThemeExtension.of(context).tint7,
FlowyTint.tint8 => AFThemeExtension.of(context).tint8,
FlowyTint.tint9 => AFThemeExtension.of(context).tint9,
Color color(BuildContext context, {AFThemeExtension? theme}) =>
switch (this) {
FlowyTint.tint1 => theme?.tint1 ?? AFThemeExtension.of(context).tint1,
FlowyTint.tint2 => theme?.tint2 ?? AFThemeExtension.of(context).tint2,
FlowyTint.tint3 => theme?.tint3 ?? AFThemeExtension.of(context).tint3,
FlowyTint.tint4 => theme?.tint4 ?? AFThemeExtension.of(context).tint4,
FlowyTint.tint5 => theme?.tint5 ?? AFThemeExtension.of(context).tint5,
FlowyTint.tint6 => theme?.tint6 ?? AFThemeExtension.of(context).tint6,
FlowyTint.tint7 => theme?.tint7 ?? AFThemeExtension.of(context).tint7,
FlowyTint.tint8 => theme?.tint8 ?? AFThemeExtension.of(context).tint8,
FlowyTint.tint9 => theme?.tint9 ?? AFThemeExtension.of(context).tint9,
};
String get id => switch (this) {

View File

@ -61,8 +61,8 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "5b3878d"
resolved-ref: "5b3878dcc5876ae7a329b308ff82763f02cf8c5f"
ref: "817c965"
resolved-ref: "817c965f26804efc09ecf97b1fb9d7d4e139ef4b"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "4.0.0"
@ -434,6 +434,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
defer_pointer:
dependency: "direct main"
description:
name: defer_pointer
sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02
url: "https://pub.dev"
source: hosted
version: "0.0.2"
desktop_drop:
dependency: "direct main"
description:

View File

@ -43,6 +43,7 @@ dependencies:
cross_file: ^0.3.4+1
# Desktop Drop uses Cross File (XFile) data type
defer_pointer: ^0.0.2
desktop_drop: ^0.4.4
device_info_plus:
diffutil_dart: ^4.0.1
@ -172,7 +173,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "5b3878d"
ref: "817c965"
appflowy_editor_plugins:
git:

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -0,0 +1,162 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy_backend/log.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Simple table markdown:', () {
setUpAll(() {
Log.shared.disableLog = true;
});
tearDownAll(() {
Log.shared.disableLog = false;
});
test('convert simple table to markdown (1)', () async {
final tableNode = createSimpleTableBlockNode(
columnCount: 7,
rowCount: 11,
contentBuilder: (rowIndex, columnIndex) =>
_sampleContents[rowIndex][columnIndex],
);
final markdown = const SimpleTableNodeParser().transform(
tableNode,
null,
);
expect(markdown,
'''|Index|Customer Id|First Name|Last Name|Company|City|Country|
|---|---|---|---|---|---|---|
|1|DD37Cf93aecA6Dc|Sheryl|Baxter|Rasmussen Group|East Leonard|Chile|
|2|1Ef7b82A4CAAD10|Preston|Lozano|Vega-Gentry|East Jimmychester|Djibouti|
|3|6F94879bDAfE5a6|Roy|Berry|Murillo-Perry|Isabelborough|Antigua and Barbuda|
|4|5Cef8BFA16c5e3c|Linda|Olsen|Dominguez, Mcmillan and Donovan|Bensonview|Dominican Republic|
|5|053d585Ab6b3159|Joanna|Bender|Martin, Lang and Andrade|West Priscilla|Slovakia (Slovak Republic)|
|6|2d08FB17EE273F4|Aimee|Downs|Steele Group|Chavezborough|Bosnia and Herzegovina|
|7|EAd384DfDbBf77|Darren|Peck|Lester, Woodard and Mitchell|Lake Ana|Pitcairn Islands|
|8|0e04AFde9f225dE|Brett|Mullen|Sanford, Davenport and Giles|Kimport|Bulgaria|
|9|C2dE4dEEc489ae0|Sheryl|Meyers|Browning-Simon|Robersonstad|Cyprus|
|10|8C2811a503C7c5a|Michelle|Gallagher|Beck-Hendrix|Elaineberg|Timor-Leste|
''');
});
test('convert markdown to simple table (1)', () async {
final document = customMarkdownToDocument(_sampleMarkdown1);
expect(document, isNotNull);
final tableNode = document.nodeAtPath([0])!;
expect(tableNode, isNotNull);
expect(tableNode.type, equals(SimpleTableBlockKeys.type));
expect(tableNode.rowLength, equals(4));
expect(tableNode.columnLength, equals(4));
});
});
}
const _sampleContents = <List<String>>[
[
"Index",
"Customer Id",
"First Name",
"Last Name",
"Company",
"City",
"Country",
],
[
"1",
"DD37Cf93aecA6Dc",
"Sheryl",
"Baxter",
"Rasmussen Group",
"East Leonard",
"Chile",
],
[
"2",
"1Ef7b82A4CAAD10",
"Preston",
"Lozano",
"Vega-Gentry",
"East Jimmychester",
"Djibouti",
],
[
"3",
"6F94879bDAfE5a6",
"Roy",
"Berry",
"Murillo-Perry",
"Isabelborough",
"Antigua and Barbuda",
],
[
"4",
"5Cef8BFA16c5e3c",
"Linda",
"Olsen",
"Dominguez, Mcmillan and Donovan",
"Bensonview",
"Dominican Republic",
],
[
"5",
"053d585Ab6b3159",
"Joanna",
"Bender",
"Martin, Lang and Andrade",
"West Priscilla",
"Slovakia (Slovak Republic)",
],
[
"6",
"2d08FB17EE273F4",
"Aimee",
"Downs",
"Steele Group",
"Chavezborough",
"Bosnia and Herzegovina",
],
[
"7",
"EAd384DfDbBf77",
"Darren",
"Peck",
"Lester, Woodard and Mitchell",
"Lake Ana",
"Pitcairn Islands",
],
[
"8",
"0e04AFde9f225dE",
"Brett",
"Mullen",
"Sanford, Davenport and Giles",
"Kimport",
"Bulgaria",
],
[
"9",
"C2dE4dEEc489ae0",
"Sheryl",
"Meyers",
"Browning-Simon",
"Robersonstad",
"Cyprus",
],
[
"10",
"8C2811a503C7c5a",
"Michelle",
"Gallagher",
"Beck-Hendrix",
"Elaineberg",
"Timor-Leste",
],
];
const _sampleMarkdown1 = '''|A|B|C||
|---|---|---|---|
|D|E|F||
|1|2|3||
|||||
''';

View File

@ -1,5 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart';
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';

View File

@ -1,4 +1,4 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
(EditorState editorState, Node tableNode) createEditorStateAndTable({