diff --git a/lib/src/editor/block_component/standard_block_components.dart b/lib/src/editor/block_component/standard_block_components.dart index d9c6300d2..c5708b412 100644 --- a/lib/src/editor/block_component/standard_block_components.dart +++ b/lib/src/editor/block_component/standard_block_components.dart @@ -128,6 +128,6 @@ final List standardCommandShortcutEvents = [ // copy paste and cut copyCommand, - pasteCommand, - cutCommand + ...pasteCommands, + cutCommand, ]; diff --git a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart index c177757c7..51e4f77d8 100644 --- a/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart +++ b/lib/src/editor/editor_component/service/shortcuts/command_shortcut_events/paste_command.dart @@ -1,6 +1,11 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; +final List pasteCommands = [ + pasteCommand, + pasteTextWithoutFormattingCommand, +]; + /// Paste. /// /// - support @@ -14,6 +19,52 @@ final CommandShortcutEvent pasteCommand = CommandShortcutEvent( handler: _pasteCommandHandler, ); +final CommandShortcutEvent pasteTextWithoutFormattingCommand = + CommandShortcutEvent( + key: 'paste the content', + command: 'ctrl+shift+v', + macOSCommand: 'cmd+shift+v', + handler: _pasteTextWithoutFormattingCommandHandler, +); + +CommandShortcutEventHandler _pasteTextWithoutFormattingCommandHandler = + (editorState) { + if (PlatformExtension.isMobile) { + assert( + false, + 'pasteTextWithoutFormattingCommand is not supported on mobile platform.', + ); + return KeyEventResult.ignored; + } + + var selection = editorState.selection; + if (selection == null) { + return KeyEventResult.ignored; + } + + // delete the selection first. + if (!selection.isCollapsed) { + editorState.deleteSelection(selection); + } + + // fetch selection again. + selection = editorState.selection; + if (selection == null) { + return KeyEventResult.skipRemainingHandlers; + } + assert(selection.isCollapsed); + + () async { + final data = await AppFlowyClipboard.getData(); + final text = data.text; + if (text != null && text.isNotEmpty) { + handlePastePlainText(editorState, text); + } + }(); + + return KeyEventResult.handled; +}; + CommandShortcutEventHandler _pasteCommandHandler = (editorState) { if (PlatformExtension.isMobile) { assert(false, 'pasteCommand is not supported on mobile platform.'); diff --git a/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart b/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart new file mode 100644 index 000000000..87ea7d04d --- /dev/null +++ b/test/new/service/shortcuts/command_shortcut_events/paste_command_test.dart @@ -0,0 +1,129 @@ +import 'dart:io'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../../infra/testable_editor.dart'; + +const text = 'Welcome to AppFlowy Editor 🔥!'; + +void main() async { + setUp(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + group('paste_command_test.dart paste_plaintext', () { + testWidgets('works with formatted text', (tester) async { + final editor = tester.editor..addParagraph(initialText: text); + await editor.startTesting(); + + await _applyFormatting( + editor, + BuiltInAttributeKey.underline, + LogicalKeyboardKey.keyU, + ); + + await _applyFormatting( + editor, + BuiltInAttributeKey.italic, + LogicalKeyboardKey.keyI, + ); + + await _applyFormatting( + editor, + BuiltInAttributeKey.bold, + LogicalKeyboardKey.keyB, + ); + await tester.pumpAndSettle(); + + final selection = Selection.single( + path: [0], + startOffset: 0, + endOffset: 7, + ); + + await editor.updateSelection(selection); + await editor.pressKey( + key: LogicalKeyboardKey.keyC, + isMetaPressed: Platform.isMacOS, + isControlPressed: Platform.isWindows || Platform.isLinux, + ); + + await editor.updateSelection(selection); + + await editor.pressKey( + key: LogicalKeyboardKey.keyV, + isShiftPressed: true, + isMetaPressed: Platform.isMacOS, + isControlPressed: Platform.isWindows || Platform.isLinux, + ); + + await editor.updateSelection(selection); + final node = editor.nodeAtPath([0]); + + _checkSelectionNotFormatted( + node!, + selection, + BuiltInAttributeKey.bold, + ); + + _checkSelectionNotFormatted( + node, + selection, + BuiltInAttributeKey.underline, + ); + + _checkSelectionNotFormatted( + node, + selection, + BuiltInAttributeKey.italic, + ); + + await editor.dispose(); + }); + }); +} + +Future _applyFormatting( + TestableEditor editor, + String matchStyle, + LogicalKeyboardKey key, +) async { + final selection = Selection.single( + path: [0], + startOffset: 0, + endOffset: 7, + ); + await editor.updateSelection(selection); + await editor.pressKey( + key: key, + isMetaPressed: Platform.isMacOS, + isControlPressed: Platform.isWindows || Platform.isLinux, + ); + final node = editor.nodeAtPath([0]); + + expect( + node!.allSatisfyInSelection(selection, (delta) { + return delta.whereType().every( + (el) => el.attributes?[matchStyle] == true, + ); + }), + true, + ); +} + +void _checkSelectionNotFormatted( + Node node, + Selection selection, + String matchStyle, +) { + expect( + node.allSatisfyInSelection(selection, (delta) { + return delta.whereType().every( + (el) => el.attributes?[matchStyle] != true, + ); + }), + true, + ); +}