mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support floating selection and delete textnode
This commit is contained in:
parent
f58a6c9523
commit
e1d990e4ae
@ -74,7 +74,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "1. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -83,7 +83,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "2. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -92,7 +92,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "3. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -101,7 +101,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
|
||||
"insert": "4. Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support.Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -110,7 +110,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "5. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -119,7 +119,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "6. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -128,7 +128,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "7. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -137,7 +137,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "8. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -146,7 +146,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "9. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -155,7 +155,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "10. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
@ -164,7 +164,7 @@
|
||||
"type": "text",
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click the '?' at the bottom right for help and support."
|
||||
"insert": "11. Click the '?' at the bottom right for help and support."
|
||||
}
|
||||
],
|
||||
"attributes": {}
|
||||
|
@ -96,9 +96,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
);
|
||||
return FlowyEditor(
|
||||
editorState: _editorState,
|
||||
keyEventHandler: [
|
||||
deleteSingleImageNode,
|
||||
],
|
||||
keyEventHandler: const [],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -1,17 +1,6 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
FlowyKeyEventHandler deleteSingleImageNode = (editorState, event) {
|
||||
final selectNodes = editorState.selectedNodes;
|
||||
if (selectNodes.length != 1 || selectNodes.first.type != 'image') {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
TransactionBuilder(editorState)
|
||||
..deleteNode(selectNodes.first)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
class ImageNodeBuilder extends NodeWidgetBuilder {
|
||||
ImageNodeBuilder.create({
|
||||
required super.node,
|
||||
@ -67,6 +56,11 @@ class __ImageNodeWidgetState extends State<_ImageNodeWidget> with Selectable {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getOffsetByTextSelection(TextSelection textSelection) {
|
||||
return Offset.zero;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _build(context);
|
||||
|
@ -93,6 +93,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
final selectionBaseOffset = _getTextPositionAtOffset(localStart).offset;
|
||||
final textSelection = TextSelection.collapsed(offset: selectionBaseOffset);
|
||||
_textSelection = textSelection;
|
||||
print('text selection = $textSelection');
|
||||
return _computeCursorRect(textSelection.baseOffset);
|
||||
}
|
||||
|
||||
@ -101,6 +102,12 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
return _textSelection;
|
||||
}
|
||||
|
||||
@override
|
||||
Offset getOffsetByTextSelection(TextSelection textSelection) {
|
||||
final offset = _computeCursorRect(textSelection.baseOffset).center;
|
||||
return _renderParagraph.localToGlobal(offset);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget richText;
|
||||
@ -148,6 +155,7 @@ class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
||||
final cursorOffset =
|
||||
_renderParagraph.getOffsetForCaret(position, Rect.zero);
|
||||
final cursorHeight = _renderParagraph.getFullHeightForCaret(position);
|
||||
print('offset = $offset, cursorHeight = $cursorHeight');
|
||||
if (cursorHeight != null) {
|
||||
const cursorWidth = 2;
|
||||
return Rect.fromLTWH(
|
||||
|
@ -19,6 +19,9 @@ class ApplyOptions {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO
|
||||
final selectionServiceKey = GlobalKey();
|
||||
|
||||
class EditorState {
|
||||
final StateTree document;
|
||||
final RenderPlugins renderPlugins;
|
||||
|
@ -0,0 +1,8 @@
|
||||
extension FlowyObjectExtensions on Object {
|
||||
T? unwrapOrNull<T>() {
|
||||
if (this is T) {
|
||||
return this as T;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -11,5 +11,4 @@ export 'package:flowy_editor/operation/transaction.dart';
|
||||
export 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
export 'package:flowy_editor/operation/operation.dart';
|
||||
export 'package:flowy_editor/editor_state.dart';
|
||||
export 'package:flowy_editor/service/flowy_editor_service.dart';
|
||||
export 'package:flowy_editor/service/flowy_keyboard_service.dart';
|
||||
export 'package:flowy_editor/service/editor_service.dart';
|
||||
|
@ -13,4 +13,7 @@ mixin Selectable<T extends StatefulWidget> on State<T> {
|
||||
|
||||
/// For [TextNode] only.
|
||||
TextSelection? getTextSelection();
|
||||
|
||||
/// For [TextNode] only.
|
||||
Offset getOffsetByTextSelection(TextSelection textSelection);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:flowy_editor/service/flowy_keyboard_service.dart';
|
||||
import 'package:flowy_editor/service/flowy_selection_service.dart';
|
||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_nodes_handler.dart';
|
||||
import 'package:flowy_editor/service/flowy_key_event_handlers/delete_single_text_node_handler.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/service/selection_service.dart';
|
||||
|
||||
import '../editor_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -23,11 +25,13 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowySelectionService(
|
||||
return FlowySelection(
|
||||
key: selectionServiceKey,
|
||||
editorState: editorState,
|
||||
child: FlowyKeyboardWidget(
|
||||
child: FlowyKeyboard(
|
||||
handlers: [
|
||||
flowyDeleteNodesHandler,
|
||||
deleteSingleTextNodeHandler,
|
||||
...widget.keyEventHandler,
|
||||
],
|
||||
editorState: editorState,
|
@ -0,0 +1,21 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) {
|
||||
// Handle delete nodes.
|
||||
final nodes = editorState.selectedNodes;
|
||||
if (nodes.length <= 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
debugPrint('delete nodes = $nodes');
|
||||
|
||||
nodes
|
||||
.fold<TransactionBuilder>(
|
||||
TransactionBuilder(editorState),
|
||||
(previousValue, node) => previousValue..deleteNode(node),
|
||||
)
|
||||
.commit();
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -0,0 +1,73 @@
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||
import 'package:flowy_editor/service/selection_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// TODO: need to be refactored, just a example code.
|
||||
FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final selectionNodes = editorState.selectedNodes;
|
||||
if (selectionNodes.length == 1 && selectionNodes.first is TextNode) {
|
||||
final node = selectionNodes.first.unwrapOrNull<TextNode>();
|
||||
final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
|
||||
if (selectable != null) {
|
||||
final textSelection = selectable.getTextSelection();
|
||||
if (textSelection != null) {
|
||||
if (textSelection.isCollapsed) {
|
||||
/// Three cases:
|
||||
/// Delete the zero character,
|
||||
/// 1. if there is still text node in front of it, then merge them.
|
||||
/// 2. if not, just ignore
|
||||
/// Delete the non-zero character,
|
||||
/// 3. delete the single character.
|
||||
if (textSelection.baseOffset == 0) {
|
||||
if (node?.previous != null && node?.previous is TextNode) {
|
||||
final previous = node!.previous! as TextNode;
|
||||
final newTextSelection = TextSelection.collapsed(
|
||||
offset: previous.toRawString().length);
|
||||
final selectionService =
|
||||
selectionServiceKey.currentState as FlowySelectionService;
|
||||
final previousSelectable =
|
||||
previous.key?.currentState?.unwrapOrNull<Selectable>();
|
||||
final newOfset = previousSelectable
|
||||
?.getOffsetByTextSelection(newTextSelection);
|
||||
if (newOfset != null) {
|
||||
selectionService.updateCursor(newOfset);
|
||||
}
|
||||
// merge
|
||||
TransactionBuilder(editorState)
|
||||
..deleteNode(node)
|
||||
..insertText(
|
||||
previous, previous.toRawString().length, node.toRawString())
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(node!, textSelection.baseOffset - 1, 1)
|
||||
..commit();
|
||||
final newTextSelection =
|
||||
TextSelection.collapsed(offset: textSelection.baseOffset - 1);
|
||||
final selectionService =
|
||||
selectionServiceKey.currentState as FlowySelectionService;
|
||||
final newOfset =
|
||||
selectable.getOffsetByTextSelection(newTextSelection);
|
||||
selectionService.updateCursor(newOfset);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../editor_state.dart';
|
||||
@ -9,27 +8,9 @@ typedef FlowyKeyEventHandler = KeyEventResult Function(
|
||||
RawKeyEvent event,
|
||||
);
|
||||
|
||||
FlowyKeyEventHandler flowyDeleteNodesHandler = (editorState, event) {
|
||||
// Handle delete nodes.
|
||||
final nodes = editorState.selectedNodes;
|
||||
if (nodes.length <= 1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
debugPrint('delete nodes = $nodes');
|
||||
|
||||
nodes
|
||||
.fold<TransactionBuilder>(
|
||||
TransactionBuilder(editorState),
|
||||
(previousValue, node) => previousValue..deleteNode(node),
|
||||
)
|
||||
.commit();
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
|
||||
/// Process keyboard events
|
||||
class FlowyKeyboardWidget extends StatefulWidget {
|
||||
const FlowyKeyboardWidget({
|
||||
class FlowyKeyboard extends StatefulWidget {
|
||||
const FlowyKeyboard({
|
||||
Key? key,
|
||||
required this.handlers,
|
||||
required this.editorState,
|
||||
@ -41,10 +22,10 @@ class FlowyKeyboardWidget extends StatefulWidget {
|
||||
final List<FlowyKeyEventHandler> handlers;
|
||||
|
||||
@override
|
||||
State<FlowyKeyboardWidget> createState() => _FlowyKeyboardWidgetState();
|
||||
State<FlowyKeyboard> createState() => _FlowyKeyboardState();
|
||||
}
|
||||
|
||||
class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
|
||||
class _FlowyKeyboardState extends State<FlowyKeyboard> {
|
||||
final FocusNode focusNode = FocusNode(debugLabel: 'flowy_keyboard_service');
|
||||
|
||||
@override
|
@ -8,7 +8,7 @@ import '../document/node.dart';
|
||||
import '../render/selection/selectable.dart';
|
||||
|
||||
/// Process selection and cursor
|
||||
mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
mixin FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
/// [Pan] and [Tap] must be mutually exclusive.
|
||||
/// Pan
|
||||
Offset? panStartOffset;
|
||||
@ -19,20 +19,20 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
|
||||
void updateSelection(Offset start, Offset end);
|
||||
|
||||
void updateCursor(Offset offset);
|
||||
void updateCursor(Offset start);
|
||||
|
||||
/// Returns selected node(s)
|
||||
/// Returns empty list if no nodes are being selected.
|
||||
List<Node> get selectedNodes;
|
||||
List<Node> getSelectedNodes(Offset start, [Offset? end]);
|
||||
|
||||
/// Compute selected node triggered by [Tap]
|
||||
Node? computeSelectedNodeByTap(
|
||||
Node? computeSelectedNodeInOffset(
|
||||
Node node,
|
||||
Offset offset,
|
||||
);
|
||||
|
||||
/// Compute selected nodes triggered by [Pan]
|
||||
List<Node> computeSelectedNodesByPan(
|
||||
List<Node> computeSelectedNodesInRange(
|
||||
Node node,
|
||||
Offset start,
|
||||
Offset end,
|
||||
@ -52,8 +52,8 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
|
||||
);
|
||||
}
|
||||
|
||||
class FlowySelectionService extends StatefulWidget {
|
||||
const FlowySelectionService({
|
||||
class FlowySelection extends StatefulWidget {
|
||||
const FlowySelection({
|
||||
Key? key,
|
||||
required this.editorState,
|
||||
required this.child,
|
||||
@ -63,11 +63,11 @@ class FlowySelectionService extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<FlowySelectionService> createState() => _FlowySelectionServiceState();
|
||||
State<FlowySelection> createState() => _FlowySelectionState();
|
||||
}
|
||||
|
||||
class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
with _FlowySelectionService {
|
||||
class _FlowySelectionState extends State<FlowySelection>
|
||||
with FlowySelectionService {
|
||||
final _cursorKey = GlobalKey(debugLabel: 'cursor');
|
||||
|
||||
final List<OverlayEntry> _selectionOverlays = [];
|
||||
@ -106,7 +106,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
void updateSelection(Offset start, Offset end) {
|
||||
_clearAllOverlayEntries();
|
||||
|
||||
final nodes = selectedNodes;
|
||||
final nodes = getSelectedNodes(start, end);
|
||||
editorState.selectedNodes = nodes;
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
@ -133,10 +133,10 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
}
|
||||
|
||||
@override
|
||||
void updateCursor(Offset offset) {
|
||||
void updateCursor(Offset start) {
|
||||
_clearAllOverlayEntries();
|
||||
|
||||
final nodes = selectedNodes;
|
||||
final nodes = getSelectedNodes(start);
|
||||
editorState.selectedNodes = nodes;
|
||||
if (nodes.isEmpty) {
|
||||
return;
|
||||
@ -147,7 +147,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
return;
|
||||
}
|
||||
final selectable = selectedNode.key?.currentState as Selectable;
|
||||
final rect = selectable.getCursorRect(offset);
|
||||
final rect = selectable.getCursorRect(start);
|
||||
final cursor = OverlayEntry(
|
||||
builder: ((context) => FlowyCursorWidget(
|
||||
key: _cursorKey,
|
||||
@ -161,13 +161,18 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
}
|
||||
|
||||
@override
|
||||
List<Node> get selectedNodes {
|
||||
if (panStartOffset != null && panEndOffset != null) {
|
||||
return computeSelectedNodesByPan(
|
||||
editorState.document.root, panStartOffset!, panEndOffset!);
|
||||
} else if (tapOffset != null) {
|
||||
final reuslt =
|
||||
computeSelectedNodeByTap(editorState.document.root, tapOffset!);
|
||||
List<Node> getSelectedNodes(Offset start, [Offset? end]) {
|
||||
if (end != null) {
|
||||
return computeSelectedNodesInRange(
|
||||
editorState.document.root,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
} else {
|
||||
final reuslt = computeSelectedNodeInOffset(
|
||||
editorState.document.root,
|
||||
start,
|
||||
);
|
||||
if (reuslt != null) {
|
||||
return [reuslt];
|
||||
}
|
||||
@ -176,13 +181,9 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
}
|
||||
|
||||
@override
|
||||
Node? computeSelectedNodeByTap(Node node, Offset offset) {
|
||||
assert(this.tapOffset != null);
|
||||
final tapOffset = this.tapOffset;
|
||||
if (tapOffset != null) {}
|
||||
|
||||
Node? computeSelectedNodeInOffset(Node node, Offset offset) {
|
||||
for (final child in node.children) {
|
||||
final result = computeSelectedNodeByTap(child, offset);
|
||||
final result = computeSelectedNodeInOffset(child, offset);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
@ -198,7 +199,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
}
|
||||
|
||||
@override
|
||||
List<Node> computeSelectedNodesByPan(Node node, Offset start, Offset end) {
|
||||
List<Node> computeSelectedNodesInRange(Node node, Offset start, Offset end) {
|
||||
List<Node> result = [];
|
||||
if (node.parent != null && node.key != null) {
|
||||
if (isNodeInSelection(node, start, end)) {
|
||||
@ -206,7 +207,7 @@ class _FlowySelectionServiceState extends State<FlowySelectionService>
|
||||
}
|
||||
}
|
||||
for (final child in node.children) {
|
||||
result.addAll(computeSelectedNodesByPan(child, start, end));
|
||||
result.addAll(computeSelectedNodesInRange(child, start, end));
|
||||
}
|
||||
// TODO: sort the result
|
||||
return result;
|
Loading…
Reference in New Issue
Block a user