Lucas c760a1b1fe
feat: support columns block in editor on desktop (#7402)
* feat: support columns block in editor

* feat: upgrade simple columns block

* fix: build error

* feat: add column width resizer

* fix: drag visual border

* fix: drag button position issue

* feat: add rule to check if the column is empty

* fix: flutter analyze

* feat: add document rules to delete the columns if its children are empty

* feat: support adding image in columns block

* feat: integrate block actions in columns block

* feat: support dragging to create a columns block

* feat: drag a block into an existing columns block

* feat: add delete columns and delete column rules

* feat: dragging the block to the left side of another block to create a columns block

* feat: support 2-4 columns block in slash menu

* chore: disable debug flag in columns block

* chore: update pubspec.yaml

* chore: update translations and icons

* fix: cloud integration test

* fix: integration test
2025-02-27 13:08:49 +08:00

119 lines
4.0 KiB
Dart

import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
/// Apply rules to the document
///
/// 1. ensure there is at least one paragraph in the document, otherwise the user will be blocked from typing
/// 2. remove columns block if its children are empty
class DocumentRules {
DocumentRules({
required this.editorState,
});
final EditorState editorState;
Future<void> applyRules({
required EditorTransactionValue value,
}) async {
await Future.wait([
_ensureAtLeastOneParagraphExists(value: value),
_removeColumnIfItIsEmpty(value: value),
]);
}
Future<void> _ensureAtLeastOneParagraphExists({
required EditorTransactionValue value,
}) async {
final document = editorState.document;
if (document.root.children.isEmpty) {
final transaction = editorState.transaction;
transaction
..insertNode([0], paragraphNode())
..afterSelection = Selection.collapsed(
Position(path: [0]),
);
await editorState.apply(transaction);
}
}
Future<void> _removeColumnIfItIsEmpty({
required EditorTransactionValue value,
}) async {
final transaction = value.$2;
final options = value.$3;
if (options.inMemoryUpdate) {
return;
}
for (final operation in transaction.operations) {
final deleteColumnsTransaction = editorState.transaction;
if (operation is DeleteOperation) {
final path = operation.path;
final column = editorState.document.nodeAtPath(path.parent);
if (column != null && column.type == SimpleColumnBlockKeys.type) {
// check if the column is empty
final children = column.children;
if (children.isEmpty) {
// delete the column or the columns
final columns = column.parent;
if (columns != null &&
columns.type == SimpleColumnsBlockKeys.type) {
final nonEmptyColumnCount = columns.children.fold(
0,
(p, c) => c.children.isEmpty ? p : p + 1,
);
// Example:
// columns
// - column 1
// - paragraph 1-1
// - paragraph 1-2
// - column 2
// - paragraph 2
// - column 3
// - paragraph 3
//
// case 1: delete the paragraph 3 from column 3.
// because there is only one child in column 3, we should delete the column 3 as well.
// the result should be:
// columns
// - column 1
// - paragraph 1-1
// - paragraph 1-2
// - column 2
// - paragraph 2
//
// case 2: delete the paragraph 3 from column 3 and delete the paragraph 2 from column 2.
// in this case, there will be only one column left, so we should delete the columns block and flatten the children.
// the result should be:
// paragraph 1-1
// paragraph 1-2
// if there is only one empty column left, delete the columns block and flatten the children
if (nonEmptyColumnCount <= 1) {
// move the children in columns out of the column
final children = columns.children
.map((e) => e.children)
.expand((e) => e)
.map((e) => e.deepCopy())
.toList();
deleteColumnsTransaction.insertNodes(columns.path, children);
deleteColumnsTransaction.deleteNode(columns);
} else {
// otherwise, delete the column
deleteColumnsTransaction.deleteNode(column);
}
}
}
}
}
if (deleteColumnsTransaction.operations.isNotEmpty) {
await editorState.apply(deleteColumnsTransaction);
}
}
}
}