chore: integrate delete line command into cut command (#907)
This commit is contained in:
parent
54edcadbb2
commit
c5de9ed84d
7 changed files with 84 additions and 125 deletions
|
|
@ -1,43 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
/// Delete the current line
|
|
||||||
///
|
|
||||||
/// - support
|
|
||||||
/// - desktop
|
|
||||||
/// - web
|
|
||||||
///
|
|
||||||
final CommandShortcutEvent deleteLineCommand = CommandShortcutEvent(
|
|
||||||
key: 'delete line',
|
|
||||||
getDescription: () => 'Delete the current line',
|
|
||||||
command: 'ctrl+x',
|
|
||||||
macOSCommand: 'cmd+x',
|
|
||||||
handler: _deleteLineCommandHandler,
|
|
||||||
);
|
|
||||||
|
|
||||||
// when the selection is collapsed, press cmd+x(or ctrl+x) should delete the current line
|
|
||||||
CommandShortcutEventHandler _deleteLineCommandHandler = (editorState) {
|
|
||||||
final selection = editorState.selection;
|
|
||||||
if (selection == null || !selection.isCollapsed) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
final node = editorState.getNodeAtPath(selection.end.path);
|
|
||||||
if (node == null) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
final nextNode = node.next;
|
|
||||||
Selection? afterSelection;
|
|
||||||
if (nextNode != null && nextNode.delta != null) {
|
|
||||||
afterSelection = Selection.collapsed(
|
|
||||||
Position(path: node.path, offset: nextNode.delta?.length ?? 0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
|
||||||
..deleteNode(node)
|
|
||||||
..afterSelection = afterSelection;
|
|
||||||
editorState.apply(transaction);
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
|
||||||
};
|
|
||||||
|
|
@ -6,7 +6,6 @@ export 'base_component/block_component_action_wrapper.dart';
|
||||||
export 'base_component/block_component_configuration.dart';
|
export 'base_component/block_component_configuration.dart';
|
||||||
// base
|
// base
|
||||||
export 'base_component/convert_to_paragraph_command.dart';
|
export 'base_component/convert_to_paragraph_command.dart';
|
||||||
export 'base_component/delete_line_command.dart';
|
|
||||||
export 'base_component/indent_command.dart';
|
export 'base_component/indent_command.dart';
|
||||||
export 'base_component/insert_newline_in_type_command.dart';
|
export 'base_component/insert_newline_in_type_command.dart';
|
||||||
export 'base_component/markdown_format_helper.dart';
|
export 'base_component/markdown_format_helper.dart';
|
||||||
|
|
|
||||||
|
|
@ -148,9 +148,6 @@ final List<CommandShortcutEvent> standardCommandShortcutEvents = [
|
||||||
//
|
//
|
||||||
selectAllCommand,
|
selectAllCommand,
|
||||||
|
|
||||||
// delete line
|
|
||||||
deleteLineCommand,
|
|
||||||
|
|
||||||
// copy paste and cut
|
// copy paste and cut
|
||||||
copyCommand,
|
copyCommand,
|
||||||
...pasteCommands,
|
...pasteCommands,
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ class AppFlowyClipboardData {
|
||||||
class AppFlowyClipboard {
|
class AppFlowyClipboard {
|
||||||
static AppFlowyClipboardData? _mockData;
|
static AppFlowyClipboardData? _mockData;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
static String? lastText;
|
||||||
|
|
||||||
static Future<void> setData({
|
static Future<void> setData({
|
||||||
String? text,
|
String? text,
|
||||||
String? html,
|
String? html,
|
||||||
|
|
@ -21,6 +24,8 @@ class AppFlowyClipboard {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastText = text;
|
||||||
|
|
||||||
return Clipboard.setData(
|
return Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text: text,
|
text: text,
|
||||||
|
|
|
||||||
|
|
@ -136,22 +136,38 @@ Selection _computeSelectionAfterPasteMultipleNodes(
|
||||||
|
|
||||||
void handleCopy(EditorState editorState) async {
|
void handleCopy(EditorState editorState) async {
|
||||||
final selection = editorState.selection?.normalized;
|
final selection = editorState.selection?.normalized;
|
||||||
if (selection == null || selection.isCollapsed) {
|
if (selection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final text = editorState.getTextInSelection(selection).join('\n');
|
String text;
|
||||||
final nodes = editorState.getSelectedNodes(selection: selection);
|
String html;
|
||||||
if (nodes.isEmpty) {
|
|
||||||
return;
|
if (selection.isCollapsed) {
|
||||||
}
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
final html = documentToHTML(
|
if (node == null) {
|
||||||
Document(
|
return;
|
||||||
root: Node(
|
}
|
||||||
type: 'page',
|
text = node.delta?.toPlainText() ?? '';
|
||||||
children: nodes,
|
html = documentToHTML(
|
||||||
|
Document(
|
||||||
|
root: pageNode(children: [node.copyWith()]),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
|
text = editorState.getTextInSelection(selection).join('\n');
|
||||||
|
final nodes = editorState.getSelectedNodes(selection: selection);
|
||||||
|
if (nodes.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
html = documentToHTML(
|
||||||
|
Document(
|
||||||
|
root: pageNode(
|
||||||
|
children: nodes.map((node) => node.copyWith()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return AppFlowyClipboard.setData(
|
return AppFlowyClipboard.setData(
|
||||||
text: text,
|
text: text,
|
||||||
html: html.isEmpty ? null : html,
|
html: html.isEmpty ? null : html,
|
||||||
|
|
@ -332,13 +348,30 @@ void handleCut(EditorState editorState) {
|
||||||
deleteSelectedContent(editorState);
|
deleteSelectedContent(editorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void deleteSelectedContent(EditorState editorState) async {
|
Future<void> deleteSelectedContent(EditorState editorState) async {
|
||||||
final selection = editorState.selection?.normalized;
|
final selection = editorState.selection?.normalized;
|
||||||
if (selection == null || selection.isCollapsed) {
|
if (selection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final tb = editorState.transaction;
|
final transaction = editorState.transaction;
|
||||||
await editorState.deleteSelection(selection);
|
if (selection.isCollapsed) {
|
||||||
tb.afterSelection = Selection.collapsed(selection.start);
|
// if the selection is collapsed, delete the current node
|
||||||
editorState.apply(tb);
|
final node = editorState.getNodeAtPath(selection.end.path);
|
||||||
|
if (node == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
transaction.deleteNode(node);
|
||||||
|
final nextNode = node.next;
|
||||||
|
if (nextNode != null && nextNode.delta != null) {
|
||||||
|
transaction.afterSelection = Selection.collapsed(
|
||||||
|
Position(path: node.path, offset: nextNode.delta?.length ?? 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the selection is not collapsed, delete the selection
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
transaction.afterSelection = Selection.collapsed(selection.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
await editorState.apply(transaction);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,46 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../../../infra/testable_editor.dart';
|
import '../../../infra/testable_editor.dart';
|
||||||
|
import '../../../util/util.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
});
|
});
|
||||||
group('cut_command_test.dart', () {
|
group('cut_command_test.dart', () {
|
||||||
|
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||||
|
|
||||||
testWidgets('update selection and execute cut command', (tester) async {
|
testWidgets('update selection and execute cut command', (tester) async {
|
||||||
await _testCutHandle(tester, Document.fromJson(paragraphdata));
|
await _testCutHandle(tester, Document.fromJson(paragraphdata));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('cut current line if the selection is collapsed', () async {
|
||||||
|
final document = Document.blank().addParagraphs(
|
||||||
|
2,
|
||||||
|
builder: (index) => Delta()..insert('${index + 1}. $text'),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
final selection = Selection.collapsed(
|
||||||
|
Position(path: [0], offset: text.length),
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
|
||||||
|
final result = cutCommand.execute(editorState);
|
||||||
|
expect(result, KeyEventResult.handled);
|
||||||
|
|
||||||
|
// check the clipboard
|
||||||
|
final lastText = AppFlowyClipboard.lastText!;
|
||||||
|
expect(lastText, '1. $text');
|
||||||
|
|
||||||
|
final after = editorState.getNodeAtPath([0])!;
|
||||||
|
expect(after.delta!.toPlainText(), '2. $text');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
import '../../../util/util.dart';
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
group('deleteLineCommand', () {
|
|
||||||
group('delete current line if the selection is collapsed', () {
|
|
||||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
|
||||||
|
|
||||||
test('delete in collapsed selection when the index > 0', () async {
|
|
||||||
final document = Document.blank().addParagraphs(
|
|
||||||
2,
|
|
||||||
builder: (index) => Delta()..insert('${index + 1}. $text'),
|
|
||||||
);
|
|
||||||
final editorState = EditorState(document: document);
|
|
||||||
|
|
||||||
final selection = Selection.collapsed(
|
|
||||||
Position(path: [0], offset: text.length),
|
|
||||||
);
|
|
||||||
editorState.selection = selection;
|
|
||||||
|
|
||||||
final result = deleteLineCommand.execute(editorState);
|
|
||||||
expect(result, KeyEventResult.handled);
|
|
||||||
|
|
||||||
final after = editorState.getNodeAtPath([0])!;
|
|
||||||
expect(after.delta!.toPlainText(), '2. $text');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('do nothing if the selection is not collapsed', () async {
|
|
||||||
final document = Document.blank().addParagraphs(
|
|
||||||
2,
|
|
||||||
builder: (index) => Delta()..insert('${index + 1}. $text'),
|
|
||||||
);
|
|
||||||
final editorState = EditorState(document: document);
|
|
||||||
|
|
||||||
final selection = Selection(
|
|
||||||
start: Position(path: [0], offset: 0),
|
|
||||||
end: Position(path: [0], offset: text.length),
|
|
||||||
);
|
|
||||||
editorState.selection = selection;
|
|
||||||
|
|
||||||
final result = deleteLineCommand.execute(editorState);
|
|
||||||
expect(result, KeyEventResult.ignored);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
editorState.getNodeAtPath([0])!.delta!.toPlainText(),
|
|
||||||
'1. $text',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
editorState.getNodeAtPath([1])!.delta!.toPlainText(),
|
|
||||||
'2. $text',
|
|
||||||
);
|
|
||||||
expect(editorState.selection, selection);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue