mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-11-10 10:18:57 +03:00
feat: add a floating cursor and follow the document scroll. refactor the keyboard handler to a Function.
This commit is contained in:
parent
e16444f88e
commit
a6ede7dc75
@ -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": [
|
||||
|
@ -96,6 +96,9 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
);
|
||||
return FlowyEditor(
|
||||
editorState: _editorState,
|
||||
keyEventHandler: [
|
||||
deleteSingleImageNode,
|
||||
],
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -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({
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user