From e7491e518236c8a13dd53afdfc90ebe80e1bb3fd Mon Sep 17 00:00:00 2001 From: Lucas Date: Mon, 2 Dec 2024 17:50:32 +0800 Subject: [PATCH] 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> --- .../document_with_simple_table_test.dart | 350 +++++++++++++++++- .../uncategorized/import_files_test.dart | 3 +- .../presentation/editor_configuration.dart | 6 +- .../actions/block_action_list.dart | 3 +- .../actions/block_action_option_cubit.dart | 45 ++- .../drag_to_reorder/option_button.dart | 5 +- .../copy_and_paste/custom_copy_command.dart | 41 +- .../copy_and_paste/custom_cut_command.dart | 8 +- .../keyboard_interceptor.dart | 2 +- .../openai/util/ask_ai_node_extension.dart | 3 +- .../parsers/markdown_parsers.dart | 2 + .../parsers/markdown_simple_table_parser.dart | 103 ++++++ .../parsers/simple_table_parser.dart | 86 +++++ .../presentation/editor_plugins/plugins.dart | 2 + .../shortcuts/command_shortcuts.dart | 1 - .../_shared_widget.dart} | 326 ++++++++++------ .../simple_table/simple_table.dart | 7 + .../simple_table_block_component.dart | 91 ++--- .../simple_table_cell_block_component.dart | 218 ++++++----- .../simple_table_constants.dart | 33 +- .../simple_table_more_action.dart | 60 ++- .../simple_table_content_operation.dart} | 6 +- .../simple_table_delete_operation.dart} | 6 +- .../simple_table_duplicate_operation.dart} | 6 +- .../simple_table_header_operation.dart} | 2 +- .../simple_table_insert_operation.dart} | 20 +- .../simple_table_map_operation.dart} | 4 +- .../simple_table_node_extension.dart} | 18 +- .../simple_table_operations.dart | 8 + .../simple_table_style_operation.dart} | 10 +- .../simple_table_row_block_component.dart | 4 +- .../simple_table_arrow_down_command.dart | 4 +- .../simple_table_arrow_left_command.dart | 2 +- .../simple_table_arrow_right_command.dart | 2 +- .../simple_table_arrow_up_command.dart | 4 +- .../simple_table_backspace_command.dart | 2 +- .../simple_table_command_extension.dart} | 35 +- .../simple_table_commands.dart | 18 + .../simple_table_select_all_command.dart | 46 +++ .../simple_table_tab_command.dart | 4 +- .../slash_menu/slash_menu_items.dart | 50 +-- .../shortcuts/simple_table_commands.dart | 16 - .../editor_plugins/table/table_menu.dart | 9 +- .../table_operations/table_operations.dart | 8 - .../lib/shared/markdown_to_document.dart | 16 +- .../application/export/document_exporter.dart | 14 +- .../flowy_infra/lib/theme_extension.dart | 21 +- frontend/appflowy_flutter/pubspec.lock | 12 +- frontend/appflowy_flutter/pubspec.yaml | 3 +- .../simple_table_contente_operation_test.dart | 2 +- .../simple_table_delete_operation_test.dart | 2 +- ...simple_table_duplicate_operation_test.dart | 2 +- .../simple_table_header_operation_test.dart | 2 +- .../simple_table_insert_operation_test.dart | 2 +- .../simple_table_markdown_test.dart | 162 ++++++++ .../simple_table_style_operation_test.dart | 3 +- .../simple_table_test_helper.dart | 2 +- 57 files changed, 1465 insertions(+), 457 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_parser.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shared_widget.dart => simple_table/_shared_widget.dart} (65%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table => simple_table}/simple_table_block_component.dart (85%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table => simple_table}/simple_table_cell_block_component.dart (65%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table => simple_table}/simple_table_constants.dart (80%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table => simple_table}/simple_table_more_action.dart (92%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_content_operation.dart => simple_table/simple_table_operations/simple_table_content_operation.dart} (93%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_delete_operation.dart => simple_table/simple_table_operations/simple_table_delete_operation.dart} (92%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_duplicate_operation.dart => simple_table/simple_table_operations/simple_table_duplicate_operation.dart} (93%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_header_operation.dart => simple_table/simple_table_operations/simple_table_header_operation.dart} (95%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_insert_operation.dart => simple_table/simple_table_operations/simple_table_insert_operation.dart} (89%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_map_operation.dart => simple_table/simple_table_operations/simple_table_map_operation.dart} (98%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_node_extension.dart => simple_table/simple_table_operations/simple_table_node_extension.dart} (96%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/table_operations/table_style_operation.dart => simple_table/simple_table_operations/simple_table_style_operation.dart} (95%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table => simple_table}/simple_table_row_block_component.dart (96%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_arrow_down_command.dart (94%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_arrow_left_command.dart (88%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_arrow_right_command.dart (89%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_arrow_up_command.dart (94%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_backspace_command.dart (93%) rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts/table_command_extension.dart => simple_table/simple_table_shortcuts/simple_table_command_extension.dart} (76%) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart rename frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/{table/shortcuts => simple_table/simple_table_shortcuts}/simple_table_tab_command.dart (87%) delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_commands.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart create mode 100644 frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart index f9db73d7ef..65d206c720 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_simple_table_test.dart @@ -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(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 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 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 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); + } } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart index b4179777c9..84da89f6b7 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart @@ -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, ); }); }); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart index f15159ea63..659727fd4c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_configuration.dart @@ -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; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart index bb8fe611e0..3dfb967445 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_list.dart @@ -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), ], ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart index 235d9d5efe..7239876671 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_option_cubit.dart @@ -86,6 +86,11 @@ class BlockActionOptionCubit extends Cubit { 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 { ); } + 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 = >[]; + final children = node.children; + for (var i = 0; i < rowsLen; i++) { + final row = []; + 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 _copyLinkToBlock(Node node) async { final context = editorState.document.root.context; final viewId = context?.read().documentId; @@ -447,7 +490,7 @@ class BlockActionOptionCubit extends Cubit { // 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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart index bc5ac58ff6..fa6b771b74 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/drag_to_reorder/option_button.dart @@ -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 { widget.blockComponentContext.node, beforeSelection, ); - Log.info( - 'update block selection, beforeSelection: $beforeSelection, afterSelection: $selection', - ); + widget.editorState.updateSelectionWithReason( selection, customSelectionType: SelectionType.block, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart index 39931cac27..6fa9b0b8db 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_copy_command.dart @@ -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 = []; + 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 _handleSubPageNodes(List nodes, [bool isCut = false]) { final handled = []; for (final node in nodes) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart index b5d20a3ec0..9fea34edbf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/custom_cut_command.dart @@ -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; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart index 047eed37cf..1d0bb5e49f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/keyboard_interceptor/keyboard_interceptor.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/ask_ai_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/ask_ai_node_extension.dart index 9d696d7cfe..933712217f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/ask_ai_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/openai/util/ask_ai_node_extension.dart @@ -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), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart index e57ce61df6..568baeaac0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_parsers.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart new file mode 100644 index 0000000000..47668ec0ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/markdown_simple_table_parser.dart @@ -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 transform( + md.Node element, + List 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() + .where((e) => e.tag == 'thead') + .firstOrNull + ?.children + ?.whereType() + .where((e) => e.tag == 'tr') + .expand((e) => e.children?.whereType().toList() ?? []) + .where((e) => e.tag == 'th') + .toList(); + + final tr = ec + .whereType() + .where((e) => e.tag == 'tbody') + .firstOrNull + ?.children + ?.whereType() + .where((e) => e.tag == 'tr') + .toList(); + + if (th == null || tr == null || th.isEmpty || tr.isEmpty) { + return []; + } + + final rows = []; + + // 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() + .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)]; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_parser.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_parser.dart new file mode 100644 index 0000000000..3c23a19198 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/parsers/simple_table_parser.dart @@ -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> _extractTableData(Node node) { + final tableData = >[]; + 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 _extractRowData(Node row) { + final rowData = []; + 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> 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(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 3102bceea8..da28f7f720 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart index b25ac1a880..025aac2584 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/shortcuts/command_shortcuts.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shared_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart similarity index 65% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shared_widget.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart index c504e02807..efdf8d45f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shared_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/_shared_widget.dart @@ -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 createState() => + _SimpleTableAddRowHoverButtonState(); +} + +class _SimpleTableAddRowHoverButtonState + extends State { + 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(); return ValueListenableBuilder( - valueListenable: context.read().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 createState() => + _SimpleTableAddColumnHoverButtonState(); +} - if (node.type != SimpleTableBlockKeys.type) { +class _SimpleTableAddColumnHoverButtonState + extends State { + 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().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().isHoveringOnTableBlock, + builder: (context, isHoveringOnTableBlock, _) { + return ValueListenableBuilder( + valueListenable: context.read().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().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().isHoveringOnTableBlock, + builder: (context, isHoveringOnTableBlock, child) { + return ValueListenableBuilder( + valueListenable: context.read().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 { - 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 { 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 { 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().updateColumnWidthInMemory( tableCellNode: widget.node, deltaX: details.delta.dx, ); }, onHorizontalDragEnd: (details) { + if (!isStartDragging) { + return; + } context.read().hoveringOnResizeHandle.value = null; isStartDragging = false; @@ -543,11 +659,9 @@ class SimpleTableBackgroundColorMenu extends StatefulWidget { class _SimpleTableBackgroundColorMenuState extends State { - 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(); }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart new file mode 100644 index 0000000000..a4e3dabcbf --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart similarity index 85% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart index ff97a8379b..cf64233490 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_block_component.dart @@ -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 ); } - 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 ), ), ), - 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, + ), + ], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart similarity index 65% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart index 51097498ff..1d99356696 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_cell_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_cell_block_component.dart @@ -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 createState() => - _SimpleTableCellBlockWidgetState(); + SimpleTableCellBlockWidgetState(); } -class _SimpleTableCellBlockWidgetState extends State +@visibleForTesting +class SimpleTableCellBlockWidgetState extends State with BlockComponentConfigurable, BlockComponentTextDirectionMixin, @@ -78,32 +77,45 @@ class _SimpleTableCellBlockWidgetState extends State @override late EditorState editorState = context.read(); - late SimpleTableContext simpleTableContext = - context.read(); + late SimpleTableContext? simpleTableContext = + context.read(); + + ValueNotifier 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 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 } 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( + 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 return _buildColumnBorder(); } else if (isCellInSelectedRow) { return _buildRowBorder(); + } else if (isEditingCellNotifier.value) { + return _buildEditingBorder(); } else { return _buildCellBorder(); } @@ -283,28 +313,14 @@ class _SimpleTableCellBlockWidgetState extends State /// 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 /// 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 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 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; + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart similarity index 80% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart index 142cf1695e..63d7e4dfe6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_constants.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_constants.dart @@ -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 selectingColumn = ValueNotifier(null); final ValueNotifier selectingRow = ValueNotifier(null); final ValueNotifier isSelectingTable = ValueNotifier(false); + final ValueNotifier 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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_more_action.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart similarity index 92% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_more_action.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart index 068496a8c7..86a0915f01 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_more_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_more_action.dart @@ -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 { 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().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 createState() => + _SimpleTableMoreActionListState(); +} + +class _SimpleTableMoreActionListState extends State { + // 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 _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 createState() => @@ -371,10 +394,7 @@ class SimpleTableMoreActionItem extends StatefulWidget { } class _SimpleTableMoreActionItemState extends State { - // ensure the background color menu and align menu exclusive - final mutex = PopoverMutex(); - - ValueNotifier isEnableHeader = ValueNotifier(false); + final isEnableHeader = ValueNotifier(false); @override void initState() { @@ -419,7 +439,7 @@ class _SimpleTableMoreActionItemState extends State { return SimpleTableAlignMenu( type: widget.type, tableCellNode: widget.tableCellNode, - mutex: mutex, + mutex: widget.popoverMutex, ); } @@ -427,7 +447,7 @@ class _SimpleTableMoreActionItemState extends State { return SimpleTableBackgroundColorMenu( type: widget.type, tableCellNode: widget.tableCellNode, - mutex: mutex, + mutex: widget.popoverMutex, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_content_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart similarity index 93% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_content_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart index c1d10df27b..3309a6397c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_content_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_content_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_delete_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart similarity index 92% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_delete_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart index f2e6d511c7..cb9c7f28dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_delete_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_delete_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_duplicate_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart similarity index 93% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_duplicate_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart index 8c83aa2a66..69ab62f0b2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_duplicate_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_duplicate_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_header_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_header_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart index f2c341c548..62b5879b1a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_header_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_header_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_insert_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart similarity index 89% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_insert_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart index 70ab268b0c..d430569dcf 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_insert_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_insert_operation.dart @@ -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 addRowInTable(Node node) async { - assert(node.type == SimpleTableBlockKeys.type); + Future 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. diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart similarity index 98% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart index 5b06716581..dc05e3f2c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_map_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_map_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart similarity index 96% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart index b199b6cc90..553497a459 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_node_extension.dart @@ -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) { diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart new file mode 100644 index 0000000000..ba5f6b47bd --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_operations.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_style_operation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_style_operation.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart index 99df9d769f..40d2649f01 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_style_operation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_operations/simple_table_style_operation.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart similarity index 96% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart index ccfc3f973c..3684beb9cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/simple_table_row_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_row_block_component.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_down_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart similarity index 94% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_down_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart index 125a53a807..68e2a1f879 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_down_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_down_command.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_left_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart similarity index 88% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_left_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart index 647fd86c37..f9a80ced5e 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_left_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_left_command.dart @@ -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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_right_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart similarity index 89% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_right_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart index 24b8085479..196357b5b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_right_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_right_command.dart @@ -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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_up_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart similarity index 94% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_up_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart index a3978c6eef..f6919f3b04 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_arrow_up_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_arrow_up_command.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_backspace_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart similarity index 93% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_backspace_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart index 14ea25ab3a..0ebfd013e3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_backspace_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_backspace_command.dart @@ -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'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart similarity index 76% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart index f4d49babe3..980cfcdeca 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/table_command_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_command_extension.dart @@ -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 diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart new file mode 100644 index 0000000000..b52adc6334 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_commands.dart @@ -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, +]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart new file mode 100644 index 0000000000..a33b08f66e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_select_all_command.dart @@ -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; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_tab_command.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart similarity index 87% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_tab_command.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart index a2466617c7..93b072fa88 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_tab_command.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/simple_table/simple_table_shortcuts/simple_table_tab_command.dart @@ -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( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart index f1bd344e89..3d5a32b154 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/slash_menu/slash_menu_items.dart @@ -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], ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_commands.dart deleted file mode 100644 index 8a5b53dd1e..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/shortcuts/simple_table_commands.dart +++ /dev/null @@ -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, -]; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart index b1fac34423..0abba733fb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_menu.dart @@ -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.addAfter, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart deleted file mode 100644 index 0eb64fb5b7..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/table/table_operations/table_operations.dart +++ /dev/null @@ -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'; diff --git a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart index 9a000e9e3c..328c66eca9 100644 --- a/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart +++ b/frontend/appflowy_flutter/lib/shared/markdown_to_document.dart @@ -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(), ], ); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart index 74b8316a4b..df46ab97e7 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/export/document_exporter.dart @@ -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 _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(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart index bb44933990..a2bfd16b06 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/theme_extension.dart @@ -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) { diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index f9d70b44e6..c33f4844df 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -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: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 4654b45739..3794027cb8 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -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: diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart index 72c76485d3..c2be7d5026 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_contente_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart index dfaef53459..ca687bc0c8 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_delete_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart index 63a2672461..d8b33c7fd9 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_duplicate_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart index c19aa9206d..ef23a6ac1f 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_header_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart index b351f3dd69..b99c1342a7 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_insert_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart new file mode 100644 index 0000000000..4037958873 --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_markdown_test.dart @@ -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 = >[ + [ + "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|| +||||| +'''; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart index fc076a597d..2b20276d37 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_style_operation_test.dart @@ -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'; diff --git a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart index be6d60cc41..8c97d53564 100644 --- a/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart +++ b/frontend/appflowy_flutter/test/unit_test/simple_table/simple_table_test_helper.dart @@ -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({