feat: add a floating cursor and follow the document scroll. refactor the keyboard handler to a Function.

This commit is contained in:
Lucas.Xu 2022-07-22 15:45:57 +08:00
parent e16444f88e
commit a6ede7dc75
10 changed files with 132 additions and 70 deletions

View File

@ -3,6 +3,12 @@
"type": "editor",
"attributes": {},
"children": [
{
"type": "image",
"attributes": {
"image_src": "https://images.squarespace-cdn.com/content/v1/617f6f16b877c06711e87373/c3f23723-37f4-44d7-9c5d-6e2a53064ae7/Asset+10.png?format=1500w"
}
},
{
"type": "text",
"delta": [

View File

@ -96,6 +96,9 @@ class _MyHomePageState extends State<MyHomePage> {
);
return FlowyEditor(
editorState: _editorState,
keyEventHandler: [
deleteSingleImageNode,
],
);
}
},

View File

@ -1,6 +1,17 @@
import 'package:flowy_editor/flowy_editor.dart';
import 'package:flowy_editor/flowy_keyboard_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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({

View File

@ -11,6 +11,8 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
final Attributes attributes;
GlobalKey? key;
// TODO: abstract a selectable node??
final layerLink = LayerLink();
String? get subtype {
// TODO: make 'subtype' as a const value.
@ -186,6 +188,7 @@ class TextNode extends Node {
return map;
}
// TODO: It's unneccesry to compute everytime.
String toRawString() =>
_delta.operations.whereType<TextInsert>().map((op) => op.content).join();
}

View File

@ -0,0 +1,60 @@
import 'dart:async';
import 'package:flutter/material.dart';
class FlowyCursorWidget extends StatefulWidget {
const FlowyCursorWidget({
Key? key,
required this.layerLink,
required this.rect,
required this.color,
this.blinkingInterval = 0.5,
}) : super(key: key);
final double blinkingInterval;
final Color color;
final Rect rect;
final LayerLink layerLink;
@override
State<FlowyCursorWidget> createState() => _FlowyCursorWidgetState();
}
class _FlowyCursorWidgetState extends State<FlowyCursorWidget> {
bool showCursor = true;
late Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(
Duration(milliseconds: (widget.blinkingInterval * 1000).toInt()),
(timer) {
setState(() {
showCursor = !showCursor;
});
});
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned.fromRect(
rect: widget.rect,
child: CompositedTransformFollower(
link: widget.layerLink,
offset: Offset(widget.rect.center.dx, 0),
showWhenUnlinked: true,
child: Container(
color: showCursor ? widget.color : Colors.transparent,
),
),
);
}
}

View File

@ -12,3 +12,4 @@ 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/flowy_editor_service.dart';
export 'package:flowy_editor/flowy_keyboard_service.dart';

View File

@ -8,9 +8,11 @@ class FlowyEditor extends StatefulWidget {
const FlowyEditor({
Key? key,
required this.editorState,
required this.keyEventHandler,
}) : super(key: key);
final EditorState editorState;
final List<FlowyKeyEventHandler> keyEventHandler;
@override
State<FlowyEditor> createState() => _FlowyEditorState();
@ -25,9 +27,8 @@ class _FlowyEditorState extends State<FlowyEditor> {
editorState: editorState,
child: FlowyKeyboardWidget(
handlers: [
FlowyKeyboradBackSpaceHandler(
editorState: editorState,
)
flowyDeleteNodesHandler,
...widget.keyEventHandler,
],
editorState: editorState,
child: editorState.build(context),

View File

@ -1,53 +1,31 @@
import 'package:flowy_editor/document/node.dart';
import 'package:flowy_editor/operation/transaction.dart';
import 'package:flowy_editor/operation/transaction_builder.dart';
import 'package:flowy_editor/render/selectable.dart';
import 'package:flutter/services.dart';
import 'editor_state.dart';
import 'package:flutter/material.dart';
abstract class FlowyKeyboardHandler {
final EditorState editorState;
typedef FlowyKeyEventHandler = KeyEventResult Function(
EditorState editorState,
RawKeyEvent event,
);
FlowyKeyboardHandler({
required this.editorState,
});
KeyEventResult onKeyDown(RawKeyEvent event);
}
class FlowyKeyboradBackSpaceHandler extends FlowyKeyboardHandler {
FlowyKeyboradBackSpaceHandler({
required super.editorState,
});
@override
KeyEventResult onKeyDown(RawKeyEvent event) {
final selectedNodes = editorState.selectedNodes;
if (selectedNodes.isNotEmpty) {
// handle delete text
// TODO: type: cursor or selection
if (selectedNodes.length == 1) {
final node = selectedNodes.first;
if (node is TextNode) {
final selectable = node.key?.currentState as Selectable?;
final textSelection = selectable?.getTextSelection();
if (textSelection != null) {
if (textSelection.isCollapsed) {
TransactionBuilder(editorState)
..deleteText(node, textSelection.start - 1, 1)
..commit();
// TODO: update selection??
}
}
}
}
return KeyEventResult.handled;
}
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 {
@ -60,7 +38,7 @@ class FlowyKeyboardWidget extends StatefulWidget {
final EditorState editorState;
final Widget child;
final List<FlowyKeyboardHandler> handlers;
final List<FlowyKeyEventHandler> handlers;
@override
State<FlowyKeyboardWidget> createState() => _FlowyKeyboardWidgetState();
@ -89,7 +67,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
for (final handler in widget.handlers) {
debugPrint('handle keyboard event $event by $handler');
KeyEventResult result = handler.onKeyDown(event);
KeyEventResult result = handler(widget.editorState, event);
switch (result) {
case KeyEventResult.handled:
@ -97,7 +75,7 @@ class _FlowyKeyboardWidgetState extends State<FlowyKeyboardWidget> {
case KeyEventResult.skipRemainingHandlers:
return KeyEventResult.skipRemainingHandlers;
case KeyEventResult.ignored:
break;
continue;
}
}

View File

@ -1,3 +1,4 @@
import 'package:flowy_editor/flowy_cursor_widget.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -15,9 +16,9 @@ mixin _FlowySelectionService<T extends StatefulWidget> on State<T> {
/// Tap
Offset? tapOffset;
void updateSelection();
void updateSelection(Offset start, Offset end);
void updateCursor();
void updateCursor(Offset offset);
/// Returns selected node(s)
/// Returns empty list if no nodes are being selected.
@ -66,6 +67,8 @@ class FlowySelectionWidget extends StatefulWidget {
class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
with _FlowySelectionService {
final _cursorKey = GlobalKey(debugLabel: 'cursor');
List<OverlayEntry> selectionOverlays = [];
EditorState get editorState => widget.editorState;
@ -98,14 +101,12 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
}
@override
void updateSelection() {
void updateSelection(Offset start, Offset end) {
_clearOverlay();
final nodes = selectedNodes;
editorState.selectedNodes = nodes;
if (nodes.isEmpty || panStartOffset == null || panEndOffset == null) {
assert(panStartOffset == null);
assert(panEndOffset == null);
if (nodes.isEmpty) {
return;
}
@ -114,8 +115,8 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
continue;
}
final selectable = node.key?.currentState as Selectable;
final selectionRects = selectable.getSelectionRectsInSelection(
panStartOffset!, panEndOffset!);
final selectionRects =
selectable.getSelectionRectsInSelection(start, end);
for (final rect in selectionRects) {
final overlay = OverlayEntry(
builder: ((context) => Positioned.fromRect(
@ -132,14 +133,9 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
}
@override
void updateCursor() {
void updateCursor(Offset offset) {
_clearOverlay();
if (tapOffset == null) {
assert(tapOffset == null);
return;
}
final nodes = selectedNodes;
editorState.selectedNodes = nodes;
if (nodes.isEmpty) {
@ -151,13 +147,13 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
return;
}
final selectable = selectedNode.key?.currentState as Selectable;
final rect = selectable.getCursorRect(tapOffset!);
final rect = selectable.getCursorRect(offset);
final cursor = OverlayEntry(
builder: ((context) => Positioned.fromRect(
builder: ((context) => FlowyCursorWidget(
key: _cursorKey,
rect: rect,
child: Container(
color: Colors.blue,
),
color: Colors.red,
layerLink: selectedNode.layerLink,
)),
);
selectionOverlays.add(cursor);
@ -251,7 +247,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
panStartOffset = null;
panEndOffset = null;
updateCursor();
updateCursor(tapOffset!);
}
void _onPanStart(DragStartDetails details) {
@ -268,7 +264,7 @@ class _FlowySelectionWidgetState extends State<FlowySelectionWidget>
panEndOffset = details.globalPosition;
tapOffset = null;
updateSelection();
updateSelection(panStartOffset!, panEndOffset!);
}
void _onPanEnd(DragEndDetails details) {

View File

@ -52,7 +52,10 @@ class NodeWidgetBuilder<T extends Node> {
builder: (_, __) => Consumer<T>(
builder: ((context, value, child) {
debugPrint('Node changed, and rebuilding...');
return build(context);
return CompositedTransformTarget(
link: node.layerLink,
child: build(context),
);
}),
),
);