feat: implement transform
This commit is contained in:
parent
212928321f
commit
4f4c93e92f
29 changed files with 311 additions and 228 deletions
|
|
@ -55,3 +55,4 @@ export 'src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||||
|
|
||||||
export 'src/editor/block_component/block_component.dart';
|
export 'src/editor/block_component/block_component.dart';
|
||||||
export 'src/editor/editor_component/editor_component.dart';
|
export 'src/editor/editor_component/editor_component.dart';
|
||||||
|
export 'src/editor/transform/transform.dart';
|
||||||
|
|
|
||||||
|
|
@ -95,18 +95,18 @@ extension TextCommands on EditorState {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> insertNewLine({
|
// Future<void> insertNewLine({
|
||||||
Path? path,
|
// Path? path,
|
||||||
}) async {
|
// }) async {
|
||||||
final p = path ?? getSelection(null).start.path.next;
|
// final p = path ?? getSelection(null).start.path.next;
|
||||||
final transaction = this.transaction;
|
// final transaction = this.transaction;
|
||||||
transaction.insertNode(p, TextNode.empty());
|
// transaction.insertNode(p, TextNode.empty());
|
||||||
transaction.afterSelection = Selection.single(
|
// transaction.afterSelection = Selection.single(
|
||||||
path: p,
|
// path: p,
|
||||||
startOffset: 0,
|
// startOffset: 0,
|
||||||
);
|
// );
|
||||||
return apply(transaction);
|
// return apply(transaction);
|
||||||
}
|
// }
|
||||||
|
|
||||||
Future<void> insertNewLineAtCurrentSelection() async {
|
Future<void> insertNewLineAtCurrentSelection() async {
|
||||||
final selection = getSelection(null);
|
final selection = getSelection(null);
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ class Document {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory Document.blank() {
|
||||||
|
final root = Node(
|
||||||
|
type: 'editor',
|
||||||
|
);
|
||||||
|
return Document(
|
||||||
|
root: root,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final Node root;
|
final Node root;
|
||||||
|
|
||||||
/// Returns the node at the given [path].
|
/// Returns the node at the given [path].
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,20 @@ import 'package:appflowy_editor/src/core/document/document.dart';
|
||||||
|
|
||||||
/// [NodeIterator] is used to traverse the nodes in visual order.
|
/// [NodeIterator] is used to traverse the nodes in visual order.
|
||||||
class NodeIterator implements Iterator<Node> {
|
class NodeIterator implements Iterator<Node> {
|
||||||
|
/// Creates a NodeIterator.
|
||||||
NodeIterator({
|
NodeIterator({
|
||||||
required this.document,
|
required this.document,
|
||||||
required this.startNode,
|
required this.startNode,
|
||||||
this.endNode,
|
this.endNode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The document to iterate.
|
||||||
final Document document;
|
final Document document;
|
||||||
|
|
||||||
|
/// The node to start the iteration with.
|
||||||
final Node startNode;
|
final Node startNode;
|
||||||
|
|
||||||
|
/// The node to end the iteration with.
|
||||||
final Node? endNode;
|
final Node? endNode;
|
||||||
|
|
||||||
Node? _currentNode;
|
Node? _currentNode;
|
||||||
|
|
@ -58,7 +64,7 @@ class NodeIterator implements Iterator<Node> {
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Node> toList() {
|
List<Node> toList() {
|
||||||
final result = <Node>[];
|
final List<Node> result = [];
|
||||||
while (moveNext()) {
|
while (moveNext()) {
|
||||||
result.add(current);
|
result.add(current);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -230,25 +230,37 @@ extension TextTransaction on Transaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
void mergeText(
|
void mergeText(
|
||||||
TextNode first,
|
Node left,
|
||||||
TextNode second, {
|
Node right, {
|
||||||
int? firstOffset,
|
int? leftOffset,
|
||||||
int secondOffset = 0,
|
int rightOffset = 0,
|
||||||
}) {
|
}) {
|
||||||
final firstLength = first.delta.length;
|
final leftDelta = left.delta;
|
||||||
final secondLength = second.delta.length;
|
final rightDelta = right.delta;
|
||||||
firstOffset ??= firstLength;
|
if (leftDelta == null || rightDelta == null) {
|
||||||
updateText(
|
return;
|
||||||
first,
|
}
|
||||||
|
final leftLength = leftDelta.length;
|
||||||
|
final rightLength = rightDelta.length;
|
||||||
|
leftOffset ??= leftLength;
|
||||||
|
|
||||||
|
final composed = leftDelta
|
||||||
|
.compose(
|
||||||
Delta()
|
Delta()
|
||||||
..retain(firstOffset)
|
..retain(leftOffset)
|
||||||
..delete(firstLength - firstOffset)
|
..delete(leftLength - leftOffset)
|
||||||
..addAll(second.delta.slice(secondOffset, secondLength)),
|
..addAll(rightDelta.slice(rightOffset, rightLength)),
|
||||||
);
|
)
|
||||||
|
.toJson();
|
||||||
|
|
||||||
|
updateNode(left, {
|
||||||
|
'delta': composed,
|
||||||
|
});
|
||||||
|
|
||||||
afterSelection = Selection.collapsed(
|
afterSelection = Selection.collapsed(
|
||||||
Position(
|
Position(
|
||||||
path: first.path,
|
path: left.path,
|
||||||
offset: firstOffset,
|
offset: leftOffset,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
|
||||||
|
|
||||||
extension DeleteSelection on EditorState {
|
|
||||||
Future<void> deleteSelection(Selection? selection) async {
|
|
||||||
final transaction = this.transaction;
|
|
||||||
if (selection == null || selection.isCollapsed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final normalized = selection.normalized;
|
|
||||||
final nodes = this.selection.getNodesInSelection(normalized);
|
|
||||||
|
|
||||||
transaction.afterSelection = normalized.collapse(atStart: true);
|
|
||||||
|
|
||||||
// single line
|
|
||||||
if (nodes.length == 1) {
|
|
||||||
final node = nodes.first;
|
|
||||||
if (node.delta != null) {
|
|
||||||
transaction.deleteText2(node, normalized.startIndex, normalized.length);
|
|
||||||
} else {
|
|
||||||
transaction.deleteNode(node);
|
|
||||||
}
|
|
||||||
return apply(transaction);
|
|
||||||
} else {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
|
|
||||||
extension InsertNewLine on EditorState {
|
|
||||||
Future<void> insertNewLine2(Selection? selection) async {
|
|
||||||
if (selection == null || !selection.isCollapsed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final transaction = this.transaction;
|
|
||||||
final path = selection.start.path.next;
|
|
||||||
|
|
||||||
transaction.insertNode(
|
|
||||||
path,
|
|
||||||
Node(
|
|
||||||
type: 'paragraph',
|
|
||||||
attributes: {
|
|
||||||
'delta': Delta().toJson(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
transaction.afterSelection = Selection.collapsed(
|
|
||||||
Position(
|
|
||||||
path: path,
|
|
||||||
offset: 0,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return apply(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
// transaction
|
|
||||||
export 'transaction_delete_text.dart';
|
|
||||||
export 'transaction_insert_text.dart';
|
|
||||||
|
|
||||||
// editor state
|
|
||||||
export 'editor_state_delete_selection.dart';
|
|
||||||
export 'editor_state_insert_new_line.dart';
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
|
|
||||||
extension Extension on Transaction {
|
|
||||||
void deleteText2(
|
|
||||||
Node node,
|
|
||||||
int index,
|
|
||||||
int length,
|
|
||||||
) {
|
|
||||||
final delta = node.delta;
|
|
||||||
if (delta == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final now = delta.compose(
|
|
||||||
Delta()
|
|
||||||
..retain(index)
|
|
||||||
..delete(length),
|
|
||||||
);
|
|
||||||
|
|
||||||
updateNode(node, {
|
|
||||||
'delta': now.toJson(),
|
|
||||||
});
|
|
||||||
|
|
||||||
afterSelection = Selection.collapsed(
|
|
||||||
Position(path: node.path, offset: index),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
Future<void> onDelete(
|
Future<void> onDelete(
|
||||||
|
|
@ -8,18 +7,18 @@ Future<void> onDelete(
|
||||||
) async {
|
) async {
|
||||||
Log.input.debug('onDelete: $deletion');
|
Log.input.debug('onDelete: $deletion');
|
||||||
|
|
||||||
final selection = editorState.selection.currentSelection.value;
|
final selection = editorState.selection;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// single line
|
// single line
|
||||||
if (selection.isSingle) {
|
if (selection.isSingle) {
|
||||||
final node = editorState.selection.currentSelectedNodes.first;
|
final node = editorState.selectionService.currentSelectedNodes.first;
|
||||||
assert(node.delta != null);
|
assert(node.delta != null);
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
..deleteText2(
|
..deleteText(
|
||||||
node,
|
node,
|
||||||
deletion.deletedRange.start,
|
deletion.deletedRange.start,
|
||||||
deletion.textDeleted.length,
|
deletion.textDeleted.length,
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ Future<void> onInsert(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final selection = editorState.selection.currentSelection.value;
|
final selection = editorState.selection;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +41,7 @@ Future<void> onInsert(
|
||||||
// IME
|
// IME
|
||||||
// single line
|
// single line
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
final node = editorState.selection.currentSelectedNodes.first;
|
final node = editorState.selectionService.currentSelectedNodes.first;
|
||||||
assert(node.delta != null);
|
assert(node.delta != null);
|
||||||
|
|
||||||
final transaction = editorState.transaction
|
final transaction = editorState.transaction
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
editorState = Provider.of<EditorState>(context, listen: false);
|
editorState = Provider.of<EditorState>(context, listen: false);
|
||||||
|
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
||||||
|
|
||||||
textInputService = DeltaTextInputService(
|
textInputService = DeltaTextInputService(
|
||||||
onInsert: (insertion) => onInsert(insertion, editorState),
|
onInsert: (insertion) => onInsert(insertion, editorState),
|
||||||
|
|
@ -34,15 +35,11 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
||||||
onNonTextUpdate: onNonTextUpdate,
|
onNonTextUpdate: onNonTextUpdate,
|
||||||
onPerformAction: (action) => onPerformAction(action, editorState),
|
onPerformAction: (action) => onPerformAction(action, editorState),
|
||||||
);
|
);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
|
||||||
editorState.selection.currentSelection.addListener(_onSelectionChanged);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
editorState.selection.currentSelection.removeListener(_onSelectionChanged);
|
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +50,7 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
||||||
|
|
||||||
void _onSelectionChanged() {
|
void _onSelectionChanged() {
|
||||||
// attach the delta text input service if needed
|
// attach the delta text input service if needed
|
||||||
final selection = editorState.selection.currentSelection.value;
|
final selection = editorState.selection;
|
||||||
if (selection == null) {
|
if (selection == null) {
|
||||||
textInputService.close();
|
textInputService.close();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -73,10 +70,11 @@ class _KeyboardServiceWidgetState extends State<KeyboardServiceWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
TextEditingValue? _getCurrentTextEditingValue(Selection selection) {
|
TextEditingValue? _getCurrentTextEditingValue(Selection selection) {
|
||||||
final editableNodes = editorState.selection.currentSelectedNodes.where(
|
final editableNodes =
|
||||||
|
editorState.selectionService.currentSelectedNodes.where(
|
||||||
(element) => element.delta != null,
|
(element) => element.delta != null,
|
||||||
);
|
);
|
||||||
final selection = editorState.selection.currentSelection.value;
|
final selection = editorState.selection;
|
||||||
final composingTextRange = textInputService.composingTextRange;
|
final composingTextRange = textInputService.composingTextRange;
|
||||||
if (editableNodes.isNotEmpty && selection != null) {
|
if (editableNodes.isNotEmpty && selection != null) {
|
||||||
var text = editableNodes.fold<String>(
|
var text = editableNodes.fold<String>(
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,34 @@
|
||||||
import 'package:appflowy_editor/src/editor/editor_component/service/extensions/extensions.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/editor/block_component/block_component.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
|
|
||||||
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
/// insert a new line block
|
||||||
if (PlatformExtension.isDesktop && RawKeyboard.instance.isShiftPressed) {
|
///
|
||||||
return false;
|
/// - on desktop or web: enter
|
||||||
}
|
/// - on mobile: enter
|
||||||
|
///
|
||||||
final selection = editorState.selection.currentSelection.value;
|
|
||||||
|
|
||||||
// delete the selection
|
|
||||||
await editorState.deleteSelection(selection);
|
|
||||||
|
|
||||||
// insert a new line
|
|
||||||
await editorState.insertNewLine2(selection);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
CharacterShortcutEvent insertNewLine = CharacterShortcutEvent(
|
CharacterShortcutEvent insertNewLine = CharacterShortcutEvent(
|
||||||
key: 'insert a new line',
|
key: 'insert a new line',
|
||||||
character: '\n',
|
character: '\n',
|
||||||
handler: _insertNewLineHandler,
|
handler: _insertNewLineHandler,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
||||||
|
// on desktop or web, shift + enter to insert a '\n' character to the same line.
|
||||||
|
// so, we should return the false to let the system handle it.
|
||||||
|
if (PlatformExtension.isNotMobile && RawKeyboard.instance.isShiftPressed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.selection?.normalized;
|
||||||
|
if (selection == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the selection
|
||||||
|
await editorState.deleteSelection(selection);
|
||||||
|
// insert a new line
|
||||||
|
await editorState.insertNewLine(selection.start);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'package:appflowy_editor/src/editor/block_component/block_component.dart';
|
import 'package:appflowy_editor/src/editor/block_component/block_component.dart';
|
||||||
|
|
||||||
CharacterShortcutEventHandler _markdownBlockHandler = (editorState) async {
|
CharacterShortcutEventHandler _markdownBlockHandler = (editorState) async {
|
||||||
final selection = editorState.selection.currentSelection.value;
|
final selection = editorState.selection;
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class MobileToolbar extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
editorState.selection.updateSelection(null);
|
editorState.selectionService.updateSelection(null);
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.keyboard_hide),
|
icon: const Icon(Icons.keyboard_hide),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,37 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
|
||||||
extension SelectionTransform on EditorState {
|
extension SelectionTransform on EditorState {
|
||||||
Future<void> deleteSelection(Selection selection) async {
|
Future<bool> deleteSelection(Selection selection) async {
|
||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selection = selection.normalized;
|
||||||
final transaction = this.transaction;
|
final transaction = this.transaction;
|
||||||
final normalized = selection.normalized;
|
final nodes = getNodesInSelection(selection);
|
||||||
final nodes = this.selection.getNodesInSelection(normalized);
|
|
||||||
|
|
||||||
transaction.afterSelection = normalized.collapse(atStart: true);
|
|
||||||
|
|
||||||
if (nodes.length == 1) {
|
if (nodes.length == 1) {
|
||||||
final node = nodes.first;
|
final node = nodes.first;
|
||||||
if (node.delta != null) {
|
if (node.delta != null) {
|
||||||
transaction.deleteText(
|
transaction.deleteText(
|
||||||
node,
|
node,
|
||||||
normalized.startIndex,
|
selection.startIndex,
|
||||||
normalized.length,
|
selection.length,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
transaction.deleteNode(node);
|
transaction.deleteNode(node);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
assert(nodes.first.path < nodes.last.path);
|
||||||
for (var i = 0; i < nodes.length; i++) {
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
final node = nodes[i];
|
final node = nodes[i];
|
||||||
if (node.delta != null) {
|
if (node.delta != null) {
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
transaction.deleteText(
|
transaction.mergeText(
|
||||||
node,
|
node,
|
||||||
normalized.startIndex,
|
nodes.last,
|
||||||
node.delta!.length - normalized.startIndex,
|
leftOffset: selection.startIndex,
|
||||||
);
|
rightOffset: selection.endIndex,
|
||||||
} else if (i == nodes.length - 1) {
|
|
||||||
transaction.deleteText(
|
|
||||||
node,
|
|
||||||
0,
|
|
||||||
normalized.endIndex,
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
transaction.deleteNode(node);
|
transaction.deleteNode(node);
|
||||||
|
|
@ -48,6 +42,9 @@ extension SelectionTransform on EditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return apply(transaction);
|
transaction.afterSelection = selection.collapse(atStart: true);
|
||||||
|
|
||||||
|
await apply(transaction);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ extension TextTransforms on EditorState {
|
||||||
///
|
///
|
||||||
/// Then it inserts a new paragraph node. After that, it sets the selection to be at the
|
/// Then it inserts a new paragraph node. After that, it sets the selection to be at the
|
||||||
/// beginning of the new paragraph.
|
/// beginning of the new paragraph.
|
||||||
Future<void> insertNewLine({
|
Future<void> insertNewLine(
|
||||||
Position? at,
|
Position? position,
|
||||||
}) async {
|
) async {
|
||||||
// If the position is not passed in, use the current selection.
|
// If the position is not passed in, use the current selection.
|
||||||
final position = at ?? selection.currentSelection.value?.start;
|
position = position ?? selectionService.currentSelection.value?.start;
|
||||||
|
|
||||||
// If there is no position, or if the selection is not collapsed, do nothing.
|
// If there is no position, or if the selection is not collapsed, do nothing.
|
||||||
if (position == null ||
|
if (position == null ||
|
||||||
!(selection.currentSelection.value?.isCollapsed ?? false)) {
|
!(selectionService.currentSelection.value?.isCollapsed ?? false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
2
lib/src/editor/transform/transform.dart
Normal file
2
lib/src/editor/transform/transform.dart
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export 'selection_transform.dart';
|
||||||
|
export 'text_transform.dart';
|
||||||
|
|
@ -4,4 +4,5 @@ extension PlatformExtension on Platform {
|
||||||
static bool get isDesktop =>
|
static bool get isDesktop =>
|
||||||
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
Platform.isWindows || Platform.isLinux || Platform.isMacOS;
|
||||||
static bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
static bool get isMobile => Platform.isAndroid || Platform.isIOS;
|
||||||
|
static bool get isNotMobile => !isMobile;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,17 @@ enum CursorUpdateReason {
|
||||||
class EditorState {
|
class EditorState {
|
||||||
final Document document;
|
final Document document;
|
||||||
|
|
||||||
|
final ValueNotifier<Selection?> selectionNotifier =
|
||||||
|
ValueNotifier<Selection?>(null);
|
||||||
|
Selection? get selection => selectionNotifier.value;
|
||||||
|
set selection(Selection? value) {
|
||||||
|
selectionNotifier.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
// Service reference.
|
// Service reference.
|
||||||
final service = FlowyService();
|
final service = FlowyService();
|
||||||
|
|
||||||
AppFlowySelectionService get selection => service.selectionService;
|
AppFlowySelectionService get selectionService => service.selectionService;
|
||||||
AppFlowyRenderPluginService get renderer => service.renderPluginService;
|
AppFlowyRenderPluginService get renderer => service.renderPluginService;
|
||||||
|
|
||||||
List<CharacterShortcutEvent> characterShortcutEvents = [
|
List<CharacterShortcutEvent> characterShortcutEvents = [
|
||||||
|
|
@ -134,33 +141,72 @@ class EditorState {
|
||||||
///
|
///
|
||||||
/// The options can be used to determine whether the editor
|
/// The options can be used to determine whether the editor
|
||||||
/// should record the transaction in undo/redo stack.
|
/// should record the transaction in undo/redo stack.
|
||||||
|
///
|
||||||
|
/// The maximumRuleApplyLoop is used to prevent infinite loop.
|
||||||
|
///
|
||||||
|
/// The withUpdateSelection is used to determine whether the editor
|
||||||
|
/// should update the selection after applying the transaction.
|
||||||
Future<void> apply(
|
Future<void> apply(
|
||||||
Transaction transaction, {
|
Transaction transaction, {
|
||||||
|
bool isRemote = false,
|
||||||
ApplyOptions options = const ApplyOptions(recordUndo: true),
|
ApplyOptions options = const ApplyOptions(recordUndo: true),
|
||||||
ruleCount = 0,
|
bool withUpdateSelection = true,
|
||||||
withUpdateCursor = true,
|
|
||||||
}) async {
|
}) async {
|
||||||
|
if (!editable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final completer = Completer<void>();
|
final completer = Completer<void>();
|
||||||
|
|
||||||
if (!editable) {
|
for (final operation in transaction.operations) {
|
||||||
completer.complete();
|
_applyOperation(operation);
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
// TODO: validate the transaction.
|
|
||||||
for (final op in transaction.operations) {
|
|
||||||
_applyOperation(op);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// broadcast to other users here
|
||||||
_observer.add(transaction);
|
_observer.add(transaction);
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
_recordRedoOrUndo(options, transaction);
|
||||||
_applyRules(ruleCount);
|
|
||||||
if (withUpdateCursor) {
|
|
||||||
await updateCursorSelection(transaction.afterSelection);
|
|
||||||
}
|
|
||||||
completer.complete();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (withUpdateSelection) {
|
||||||
|
selection = transaction.afterSelection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: execute this line after the UI has been updated.
|
||||||
|
{
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get nodes in selection
|
||||||
|
///
|
||||||
|
/// if selection is backward, return nodes in order
|
||||||
|
/// if selection is forward, return nodes in reverse order
|
||||||
|
///
|
||||||
|
List<Node> getNodesInSelection(Selection selection) {
|
||||||
|
// Normalize the selection.
|
||||||
|
final normalized = selection.normalized;
|
||||||
|
|
||||||
|
// Get the start and end nodes.
|
||||||
|
final startNode = document.nodeAtPath(normalized.start.path);
|
||||||
|
final endNode = document.nodeAtPath(normalized.end.path);
|
||||||
|
|
||||||
|
// If we have both nodes, we can find the nodes in the selection.
|
||||||
|
if (startNode != null && endNode != null) {
|
||||||
|
final nodes = NodeIterator(
|
||||||
|
document: document,
|
||||||
|
startNode: startNode,
|
||||||
|
endNode: endNode,
|
||||||
|
).toList();
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we don't have both nodes, we can't find the nodes in the selection.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _recordRedoOrUndo(ApplyOptions options, Transaction transaction) {
|
||||||
if (options.recordUndo) {
|
if (options.recordUndo) {
|
||||||
final undoItem = undoManager.getUndoHistoryItem();
|
final undoItem = undoManager.getUndoHistoryItem();
|
||||||
undoItem.addAll(transaction.operations);
|
undoItem.addAll(transaction.operations);
|
||||||
|
|
@ -177,33 +223,6 @@ class EditorState {
|
||||||
redoItem.afterSelection = transaction.afterSelection;
|
redoItem.afterSelection = transaction.afterSelection;
|
||||||
undoManager.redoStack.push(redoItem);
|
undoManager.redoStack.push(redoItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
return completer.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get nodes in selection
|
|
||||||
///
|
|
||||||
List<Node> getNodesInSelection(Selection selection) {
|
|
||||||
final start =
|
|
||||||
selection.isBackward ? selection.start.path : selection.end.path;
|
|
||||||
final end =
|
|
||||||
selection.isBackward ? selection.end.path : selection.start.path;
|
|
||||||
assert(start <= end);
|
|
||||||
final startNode = editorState.document.nodeAtPath(start);
|
|
||||||
final endNode = editorState.document.nodeAtPath(end);
|
|
||||||
if (startNode != null && endNode != null) {
|
|
||||||
final nodes = NodeIterator(
|
|
||||||
document: editorState.document,
|
|
||||||
startNode: startNode,
|
|
||||||
endNode: endNode,
|
|
||||||
).toList();
|
|
||||||
if (selection.isBackward) {
|
|
||||||
return nodes;
|
|
||||||
} else {
|
|
||||||
return nodes.reversed.toList(growable: false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _debouncedSealHistoryItem() {
|
void _debouncedSealHistoryItem() {
|
||||||
|
|
@ -233,9 +252,9 @@ class EditorState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _applyRules(int ruleCount) {
|
void _applyRules(int maximumRuleApplyLoop) {
|
||||||
// Set a maximum count to prevent a dead loop.
|
// Set a maximum count to prevent a dead loop.
|
||||||
if (ruleCount >= 5 || disableRules) {
|
if (maximumRuleApplyLoop >= 5 || disableRules) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -243,7 +262,10 @@ class EditorState {
|
||||||
_insureLastNodeEditable(transaction);
|
_insureLastNodeEditable(transaction);
|
||||||
|
|
||||||
if (transaction.operations.isNotEmpty) {
|
if (transaction.operations.isNotEmpty) {
|
||||||
apply(transaction, ruleCount: ruleCount + 1, withUpdateCursor: false);
|
apply(
|
||||||
|
transaction,
|
||||||
|
withUpdateSelection: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue