feat: add document rules (#1071)
This commit is contained in:
parent
a583d139d2
commit
f2685cb900
6 changed files with 120 additions and 0 deletions
|
|
@ -6,6 +6,8 @@ export 'document/document.dart';
|
|||
export 'document/node.dart';
|
||||
export 'document/node_iterator.dart';
|
||||
export 'document/path.dart';
|
||||
export 'document/rules/at_least_one_editable_node_rule.dart';
|
||||
export 'document/rules/document_rule.dart';
|
||||
export 'document/text_delta.dart';
|
||||
export 'legacy/built_in_attribute_keys.dart';
|
||||
export 'location/position.dart';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
/// This rule ensures that there is at least one editable node in the document.
|
||||
///
|
||||
/// If the document is empty, it will create a new paragraph node.
|
||||
class AtLeastOneEditableNodeRule extends DocumentRule {
|
||||
const AtLeastOneEditableNodeRule();
|
||||
@override
|
||||
bool shouldApply({
|
||||
required EditorState editorState,
|
||||
required EditorTransactionValue value,
|
||||
}) {
|
||||
final time = value.$1;
|
||||
if (time != TransactionTime.after) {
|
||||
return false;
|
||||
}
|
||||
final document = editorState.document;
|
||||
return document.root.children.isEmpty;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> apply({
|
||||
required EditorState editorState,
|
||||
required EditorTransactionValue value,
|
||||
}) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction
|
||||
..insertNode([0], paragraphNode())
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: [0]),
|
||||
);
|
||||
editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
17
lib/src/core/document/rules/document_rule.dart
Normal file
17
lib/src/core/document/rules/document_rule.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
abstract class DocumentRule {
|
||||
const DocumentRule();
|
||||
|
||||
/// Whether the rule should be applied.
|
||||
bool shouldApply({
|
||||
required EditorState editorState,
|
||||
required EditorTransactionValue value,
|
||||
});
|
||||
|
||||
/// Apply the rule to the document.
|
||||
Future<void> apply({
|
||||
required EditorState editorState,
|
||||
required EditorTransactionValue value,
|
||||
});
|
||||
}
|
||||
|
|
@ -46,6 +46,7 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
this.disableScrollService = false,
|
||||
this.disableAutoScroll = false,
|
||||
this.autoScrollEdgeOffset = appFlowyEditorAutoScrollEdgeOffset,
|
||||
this.documentRules = const [],
|
||||
}) : blockComponentBuilders =
|
||||
blockComponentBuilders ?? standardBlockComponentBuilderMap,
|
||||
characterShortcutEvents =
|
||||
|
|
@ -225,6 +226,10 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
///
|
||||
final double autoScrollEdgeOffset;
|
||||
|
||||
/// The rules to apply to the document.
|
||||
///
|
||||
final List<DocumentRule> documentRules;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||
}
|
||||
|
|
@ -372,6 +377,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||
editorState.autoCompleteTextProvider = widget.autoCompleteTextProvider;
|
||||
editorState.disableAutoScroll = widget.disableAutoScroll;
|
||||
editorState.autoScrollEdgeOffset = widget.autoScrollEdgeOffset;
|
||||
editorState.documentRules = widget.documentRules;
|
||||
}
|
||||
|
||||
BlockComponentRendererService get _renderer => BlockComponentRenderer(
|
||||
|
|
|
|||
|
|
@ -216,6 +216,8 @@ class EditorState {
|
|||
Stream<EditorTransactionValue> get transactionStream => _observer.stream;
|
||||
final StreamController<EditorTransactionValue> _observer =
|
||||
StreamController.broadcast(sync: true);
|
||||
final StreamController<EditorTransactionValue> _asyncObserver =
|
||||
StreamController.broadcast();
|
||||
|
||||
/// Store the toggled format style, like bold, italic, etc.
|
||||
/// All the values must be the key from [AppFlowyRichTextKeys.supportToggled].
|
||||
|
|
@ -267,6 +269,24 @@ class EditorState {
|
|||
// only used for testing
|
||||
bool disableSealTimer = false;
|
||||
|
||||
/// The rules to apply to the document.
|
||||
List<DocumentRule> get documentRules => _documentRules;
|
||||
List<DocumentRule> _documentRules = [];
|
||||
set documentRules(List<DocumentRule> value) {
|
||||
_documentRules = value;
|
||||
|
||||
_subscription?.cancel();
|
||||
_subscription = _asyncObserver.stream.listen((value) async {
|
||||
for (final rule in _documentRules) {
|
||||
if (rule.shouldApply(editorState: this, value: value)) {
|
||||
await rule.apply(editorState: this, value: value);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@Deprecated('use editorState.selection instead')
|
||||
Selection? _cursorSelection;
|
||||
|
||||
|
|
@ -354,11 +374,13 @@ class EditorState {
|
|||
void dispose() {
|
||||
isDisposed = true;
|
||||
_observer.close();
|
||||
_asyncObserver.close();
|
||||
_debouncedSealHistoryItemTimer?.cancel();
|
||||
onDispose.value += 1;
|
||||
onDispose.dispose();
|
||||
document.dispose();
|
||||
selectionNotifier.dispose();
|
||||
_subscription?.cancel();
|
||||
_onScrollViewScrolledListeners.clear();
|
||||
}
|
||||
|
||||
|
|
@ -398,6 +420,10 @@ class EditorState {
|
|||
_observer.add((TransactionTime.before, transaction, options));
|
||||
}
|
||||
|
||||
if (!_asyncObserver.isClosed) {
|
||||
_asyncObserver.add((TransactionTime.before, transaction, options));
|
||||
}
|
||||
|
||||
_applyTransactionInLocal(transaction);
|
||||
|
||||
// broadcast to other users here, after applying the transaction
|
||||
|
|
@ -405,6 +431,10 @@ class EditorState {
|
|||
_observer.add((TransactionTime.after, transaction, options));
|
||||
}
|
||||
|
||||
if (!_asyncObserver.isClosed) {
|
||||
_asyncObserver.add((TransactionTime.after, transaction, options));
|
||||
}
|
||||
|
||||
_recordRedoOrUndo(options, transaction, skipHistoryDebounce);
|
||||
|
||||
if (withUpdateSelection) {
|
||||
|
|
|
|||
31
test/core/rules/at_least_one_editable_node_rule_test.dart
Normal file
31
test/core/rules/at_least_one_editable_node_rule_test.dart
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
test('at least one editable node rule', () async {
|
||||
final rule = AtLeastOneEditableNodeRule();
|
||||
final node1 = paragraphNode(text: 'Hello World!');
|
||||
final node2 = paragraphNode(text: 'Hello World!');
|
||||
final node3 = paragraphNode(text: 'Hello World!');
|
||||
final document = Document(
|
||||
root: pageNode(
|
||||
children: [
|
||||
node1,
|
||||
node2,
|
||||
node3,
|
||||
],
|
||||
),
|
||||
);
|
||||
final editorState = EditorState(document: document)..documentRules = [rule];
|
||||
// delete all the nodes
|
||||
final transaction = editorState.transaction;
|
||||
transaction.deleteNodes([node1, node2, node3]);
|
||||
await editorState.apply(transaction);
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
final node = editorState.document.root.children[0];
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
|
||||
editorState.dispose();
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue