mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-12 07:34:28 +00:00
feat: add option to paste plain text (#7045)
* feat: add option to paste plain text * refactor: optimize the code Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Co-authored-by: Lucas <lucas.xu@appflowy.io> --------- Co-authored-by: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Co-authored-by: Lucas <lucas.xu@appflowy.io>
This commit is contained in:
parent
200b367e4c
commit
3959cdba3a
@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.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';
|
||||
@ -27,10 +26,13 @@ void main() {
|
||||
await tester.pasteContent(
|
||||
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 3);
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final text =
|
||||
editorState.document.root.children.first.delta!.toPlainText();
|
||||
final textLines = text.split('\n');
|
||||
for (var i = 0; i < lines; i++) {
|
||||
expect(
|
||||
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
||||
textLines[i],
|
||||
'line $i',
|
||||
);
|
||||
}
|
||||
@ -467,6 +469,44 @@ void main() {
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
#### I'm h4
|
||||
##### I'm h5
|
||||
###### I'm h6''';
|
||||
|
||||
testWidgets('paste markdowns', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
expect(text, 'I\'m h$i');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste markdowns as plain', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
pasteAsPlain: true,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
final expectText = '${'#' * i} I\'m h$i';
|
||||
expect(text, expectText);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
@ -476,6 +516,7 @@ extension on WidgetTester {
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
bool pasteAsPlain = false,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
@ -502,6 +543,7 @@ extension on WidgetTester {
|
||||
await simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
|
||||
@ -37,12 +37,12 @@ void main() {
|
||||
|
||||
// set clipboard data
|
||||
final data = [
|
||||
"123456\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"1234567\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"12345678\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"123456\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"1234567\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"12345678\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
].join();
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
|
||||
@ -13,6 +13,11 @@ final List<List<ContextMenuItem>> customContextMenuItems = [
|
||||
getName: LocaleKeys.document_plugins_contextMenu_paste.tr,
|
||||
onPressed: (editorState) => customPasteCommand.execute(editorState),
|
||||
),
|
||||
ContextMenuItem(
|
||||
getName: LocaleKeys.document_plugins_contextMenu_pasteAsPlainText.tr,
|
||||
onPressed: (editorState) =>
|
||||
customPastePlainTextCommand.execute(editorState),
|
||||
),
|
||||
ContextMenuItem(
|
||||
getName: LocaleKeys.document_plugins_contextMenu_cut.tr,
|
||||
onPressed: (editorState) => customCutCommand.execute(editorState),
|
||||
|
||||
@ -29,6 +29,14 @@ final CommandShortcutEvent customPasteCommand = CommandShortcutEvent(
|
||||
handler: _pasteCommandHandler,
|
||||
);
|
||||
|
||||
final CommandShortcutEvent customPastePlainTextCommand = CommandShortcutEvent(
|
||||
key: 'paste the plain content',
|
||||
getDescription: () => AppFlowyEditorL10n.current.cmdPasteContent,
|
||||
command: 'ctrl+shift+v',
|
||||
macOSCommand: 'cmd+shift+v',
|
||||
handler: _pastePlainCommandHandler,
|
||||
);
|
||||
|
||||
CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
@ -45,6 +53,22 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
CommandShortcutEventHandler _pastePlainCommandHandler = (editorState) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
doPlainPaste(editorState).then((_) {
|
||||
final context = editorState.document.root.context;
|
||||
if (context != null && context.mounted) {
|
||||
context.read<ClipboardState>().didPaste();
|
||||
}
|
||||
});
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
Future<void> doPaste(EditorState editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
@ -119,7 +143,7 @@ Future<void> doPaste(EditorState editorState) async {
|
||||
}
|
||||
|
||||
if (plainText != null && plainText.isNotEmpty) {
|
||||
await editorState.pastePlainText(plainText);
|
||||
await editorState.pasteText(plainText);
|
||||
return Log.info('Pasted plain text');
|
||||
}
|
||||
|
||||
@ -190,3 +214,24 @@ Future<bool> _pasteAsLinkPreview(
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> doPlainPaste(EditorState editorState) async {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorNotification.paste().post();
|
||||
|
||||
// dispatch the paste event
|
||||
final data = await getIt<ClipboardService>().getData();
|
||||
final plainText = data.plainText;
|
||||
if (plainText != null && plainText.isNotEmpty) {
|
||||
await editorState.pastePlainText(plainText);
|
||||
Log.info('Pasted plain text');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.info('unable to parse the clipboard content');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,14 +1,10 @@
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension PasteFromPlainText on EditorState {
|
||||
Future<void> pastePlainText(String plainText) async {
|
||||
if (await pasteHtmlIfAvailable(plainText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteSelectionIfNeeded();
|
||||
|
||||
final nodes = plainText
|
||||
.split('\n')
|
||||
.map(
|
||||
@ -16,14 +12,7 @@ extension PasteFromPlainText on EditorState {
|
||||
..replaceAll(r'\r', '')
|
||||
..trimRight(),
|
||||
)
|
||||
.map((e) {
|
||||
// parse the url content
|
||||
final Attributes attributes = {};
|
||||
if (hrefRegex.hasMatch(e)) {
|
||||
attributes[AppFlowyRichTextKeys.href] = e;
|
||||
}
|
||||
return Delta()..insert(e, attributes: attributes);
|
||||
})
|
||||
.map((e) => Delta()..insert(e))
|
||||
.map((e) => paragraphNode(delta: e))
|
||||
.toList();
|
||||
if (nodes.isEmpty) {
|
||||
@ -36,6 +25,24 @@ extension PasteFromPlainText on EditorState {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pasteText(String plainText) async {
|
||||
if (await pasteHtmlIfAvailable(plainText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteSelectionIfNeeded();
|
||||
|
||||
final nodes = customMarkdownToDocument(plainText).root.children;
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (nodes.length == 1) {
|
||||
await pasteSingleLineNode(nodes.first);
|
||||
} else {
|
||||
await pasteMultiLineNodes(nodes.toList());
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> pasteHtmlIfAvailable(String plainText) async {
|
||||
final selection = this.selection;
|
||||
if (selection == null ||
|
||||
|
||||
@ -30,6 +30,7 @@ List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
|
||||
customCopyCommand,
|
||||
customPasteCommand,
|
||||
customPastePlainTextCommand,
|
||||
customCutCommand,
|
||||
customUndoCommand,
|
||||
customRedoCommand,
|
||||
@ -43,6 +44,7 @@ List<CommandShortcutEvent> commandShortcutEvents = [
|
||||
copyCommand,
|
||||
cutCommand,
|
||||
pasteCommand,
|
||||
pasteTextWithoutFormattingCommand,
|
||||
toggleTodoListCommand,
|
||||
undoCommand,
|
||||
redoCommand,
|
||||
|
||||
@ -1847,7 +1847,8 @@
|
||||
"contextMenu": {
|
||||
"copy": "Copy",
|
||||
"cut": "Cut",
|
||||
"paste": "Paste"
|
||||
"paste": "Paste",
|
||||
"pasteAsPlainText": "Paste as plain text"
|
||||
},
|
||||
"action": "Actions",
|
||||
"database": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user