feat: support dragging to reorder block (#887)
* feat: support drag to reorder block * feat: enable drag to reorder on home page * fix: proper offset calculation for drop path * docs: update doc comments for the drop target API * fix: render issue --------- Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
This commit is contained in:
parent
200b572fa3
commit
5d1d311a2c
11 changed files with 301 additions and 33 deletions
|
|
@ -8,6 +8,7 @@ import 'package:example/pages/auto_complete_editor.dart';
|
||||||
import 'package:example/pages/collab_editor.dart';
|
import 'package:example/pages/collab_editor.dart';
|
||||||
import 'package:example/pages/collab_selection_editor.dart';
|
import 'package:example/pages/collab_selection_editor.dart';
|
||||||
import 'package:example/pages/customize_theme_for_editor.dart';
|
import 'package:example/pages/customize_theme_for_editor.dart';
|
||||||
|
import 'package:example/pages/drag_to_reorder_editor.dart';
|
||||||
import 'package:example/pages/editor.dart';
|
import 'package:example/pages/editor.dart';
|
||||||
import 'package:example/pages/editor_list.dart';
|
import 'package:example/pages/editor_list.dart';
|
||||||
import 'package:example/pages/fixed_toolbar_editor.dart';
|
import 'package:example/pages/fixed_toolbar_editor.dart';
|
||||||
|
|
@ -167,6 +168,14 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
// Theme Demo
|
// Theme Demo
|
||||||
_buildSeparator(context, 'Showcases'),
|
_buildSeparator(context, 'Showcases'),
|
||||||
|
_buildListTile(context, 'Drag to reorder', () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const DragToReorderEditor(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
_buildListTile(context, 'Markdown Editor', () {
|
_buildListTile(context, 'Markdown Editor', () {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
|
||||||
import 'package:example/home_page.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:example/home_page.dart';
|
||||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:example/pages/drag_to_reorder_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
@ -93,6 +94,9 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
||||||
editorStyle: editorStyle,
|
editorStyle: editorStyle,
|
||||||
enableAutoComplete: true,
|
enableAutoComplete: true,
|
||||||
autoCompleteTextProvider: _buildAutoCompleteTextProvider,
|
autoCompleteTextProvider: _buildAutoCompleteTextProvider,
|
||||||
|
dropTargetStyle: const AppFlowyDropTargetStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
header: Padding(
|
header: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 10.0),
|
padding: const EdgeInsets.only(bottom: 10.0),
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
|
|
@ -164,6 +168,16 @@ class _DesktopEditorState extends State<DesktopEditor> {
|
||||||
value.configuration = value.configuration.copyWith(
|
value.configuration = value.configuration.copyWith(
|
||||||
padding: (_) => const EdgeInsets.symmetric(vertical: 8.0),
|
padding: (_) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (key != PageBlockKeys.type) {
|
||||||
|
value.showActions = (_) => true;
|
||||||
|
value.actionBuilder = (context, actionState) {
|
||||||
|
return DragToReorderAction(
|
||||||
|
blockComponentContext: context,
|
||||||
|
builder: value,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
213
example/lib/pages/drag_to_reorder_editor.dart
Normal file
213
example/lib/pages/drag_to_reorder_editor.dart
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class DragToReorderEditor extends StatefulWidget {
|
||||||
|
const DragToReorderEditor({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DragToReorderEditor> createState() => _DragToReorderEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DragToReorderEditorState extends State<DragToReorderEditor> {
|
||||||
|
late final EditorState editorState;
|
||||||
|
late final EditorStyle editorStyle;
|
||||||
|
late final Map<String, BlockComponentBuilder> blockComponentBuilders;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
forceShowBlockAction = true;
|
||||||
|
editorState = _createEditorState();
|
||||||
|
editorStyle = _createEditorStyle();
|
||||||
|
blockComponentBuilders = _createBlockComponentBuilders();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
forceShowBlockAction = false;
|
||||||
|
editorState.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Drag to reorder'),
|
||||||
|
),
|
||||||
|
body: AppFlowyEditor(
|
||||||
|
editorState: editorState,
|
||||||
|
editorStyle: editorStyle,
|
||||||
|
blockComponentBuilders: blockComponentBuilders,
|
||||||
|
dropTargetStyle: const AppFlowyDropTargetStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, BlockComponentBuilder> _createBlockComponentBuilders() {
|
||||||
|
final builders = {...standardBlockComponentBuilderMap};
|
||||||
|
for (final entry in builders.entries) {
|
||||||
|
if (entry.key == PageBlockKeys.type) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final builder = entry.value;
|
||||||
|
|
||||||
|
// only customize the todo list block
|
||||||
|
if (entry.key == TodoListBlockKeys.type) {
|
||||||
|
builder.showActions = (_) => true;
|
||||||
|
builder.actionBuilder = (context, actionState) {
|
||||||
|
return DragToReorderAction(
|
||||||
|
blockComponentContext: context,
|
||||||
|
builder: builder,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builders;
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorState _createEditorState() {
|
||||||
|
final document = Document.blank()
|
||||||
|
..insert([
|
||||||
|
0,
|
||||||
|
], [
|
||||||
|
todoListNode(checked: false, text: 'Todo 1'),
|
||||||
|
todoListNode(checked: false, text: 'Todo 2'),
|
||||||
|
todoListNode(checked: false, text: 'Todo 3'),
|
||||||
|
]);
|
||||||
|
return EditorState(
|
||||||
|
document: document,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorStyle _createEditorStyle() {
|
||||||
|
return EditorStyle.desktop(
|
||||||
|
cursorWidth: 2.0,
|
||||||
|
cursorColor: Colors.black,
|
||||||
|
selectionColor: Colors.grey.shade300,
|
||||||
|
textStyleConfiguration: TextStyleConfiguration(
|
||||||
|
text: GoogleFonts.poppins(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
code: GoogleFonts.architectsDaughter(),
|
||||||
|
bold: GoogleFonts.poppins(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 200.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DragToReorderAction extends StatefulWidget {
|
||||||
|
const DragToReorderAction({
|
||||||
|
super.key,
|
||||||
|
required this.blockComponentContext,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BlockComponentContext blockComponentContext;
|
||||||
|
final BlockComponentBuilder builder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DragToReorderAction> createState() => _DragToReorderActionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DragToReorderActionState extends State<DragToReorderAction> {
|
||||||
|
late final Node node;
|
||||||
|
late final BlockComponentContext blockComponentContext;
|
||||||
|
|
||||||
|
Offset? globalPosition;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
// copy the node to avoid the node in document being updated
|
||||||
|
node = widget.blockComponentContext.node.copyWith();
|
||||||
|
blockComponentContext = BlockComponentContext(
|
||||||
|
widget.blockComponentContext.buildContext,
|
||||||
|
node,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10.0, right: 4.0),
|
||||||
|
child: Draggable<Node>(
|
||||||
|
data: node,
|
||||||
|
feedback: Opacity(
|
||||||
|
opacity: 0.7,
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: IntrinsicWidth(
|
||||||
|
child: IntrinsicHeight(
|
||||||
|
child: Provider.value(
|
||||||
|
value: context.read<EditorState>(),
|
||||||
|
child: widget.builder.build(blockComponentContext),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onDragStarted: () {
|
||||||
|
debugPrint('onDragStarted');
|
||||||
|
context.read<EditorState>().selectionService.removeDropTarget();
|
||||||
|
},
|
||||||
|
onDragUpdate: (details) {
|
||||||
|
context
|
||||||
|
.read<EditorState>()
|
||||||
|
.selectionService
|
||||||
|
.renderDropTargetForOffset(details.globalPosition);
|
||||||
|
|
||||||
|
globalPosition = details.globalPosition;
|
||||||
|
},
|
||||||
|
onDragEnd: (details) {
|
||||||
|
context.read<EditorState>().selectionService.removeDropTarget();
|
||||||
|
|
||||||
|
if (globalPosition == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = context
|
||||||
|
.read<EditorState>()
|
||||||
|
.selectionService
|
||||||
|
.getDropTargetRenderData(globalPosition!);
|
||||||
|
final acceptedPath = data?.dropPath;
|
||||||
|
debugPrint('onDragEnd, acceptedPath($acceptedPath)');
|
||||||
|
_moveNodeToNewPosition(node, acceptedPath);
|
||||||
|
},
|
||||||
|
child: const Icon(
|
||||||
|
Icons.drag_indicator_rounded,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _moveNodeToNewPosition(Node node, Path? acceptedPath) async {
|
||||||
|
if (acceptedPath == null) {
|
||||||
|
debugPrint('acceptedPath is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('move node($node) to path($acceptedPath)');
|
||||||
|
|
||||||
|
final editorState = context.read<EditorState>();
|
||||||
|
final transaction = editorState.transaction;
|
||||||
|
transaction.insertNode(acceptedPath, node.copyWith());
|
||||||
|
transaction.deleteNode(widget.blockComponentContext.node);
|
||||||
|
await editorState.apply(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -52,6 +52,12 @@ dependency_overrides:
|
||||||
appflowy_editor:
|
appflowy_editor:
|
||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
|
appflowy_editor_plugins:
|
||||||
|
git:
|
||||||
|
url: https://github.com/AppFlowy-IO/AppFlowy-plugins.git
|
||||||
|
path: "packages/appflowy_editor_plugins"
|
||||||
|
ref: "b228456"
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:nanoid/non_secure.dart';
|
import 'package:nanoid/non_secure.dart';
|
||||||
|
|
||||||
abstract class NodeExternalValues {
|
abstract class NodeExternalValues {
|
||||||
|
|
@ -225,11 +226,7 @@ final class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Node(id: $id,
|
return 'Node(id: $id, type: $type, attributes: $attributes, children: $children)';
|
||||||
type: $type,
|
|
||||||
attributes: $attributes,
|
|
||||||
children: $children,
|
|
||||||
)''';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta? get delta {
|
Delta? get delta {
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,23 @@ class _BlockComponentActionWrapperState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (forceShowBlockAction) {
|
||||||
|
alwaysShowActions = true;
|
||||||
|
showActionsNotifier.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
showActionsNotifier.dispose();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import 'package:provider/provider.dart';
|
||||||
/// 1. used to update the child widget when node is changed
|
/// 1. used to update the child widget when node is changed
|
||||||
/// ~~2. used to show block component actions~~
|
/// ~~2. used to show block component actions~~
|
||||||
/// 3. used to add the layer link to the child widget
|
/// 3. used to add the layer link to the child widget
|
||||||
class BlockComponentContainer extends StatefulWidget {
|
class BlockComponentContainer extends StatelessWidget {
|
||||||
const BlockComponentContainer({
|
const BlockComponentContainer({
|
||||||
super.key,
|
super.key,
|
||||||
required this.configuration,
|
required this.configuration,
|
||||||
|
|
@ -17,29 +17,24 @@ class BlockComponentContainer extends StatefulWidget {
|
||||||
|
|
||||||
final Node node;
|
final Node node;
|
||||||
final BlockComponentConfiguration configuration;
|
final BlockComponentConfiguration configuration;
|
||||||
|
|
||||||
final WidgetBuilder builder;
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
@override
|
|
||||||
State<BlockComponentContainer> createState() =>
|
|
||||||
BlockComponentContainerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class BlockComponentContainerState extends State<BlockComponentContainer> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ChangeNotifierProvider<Node>.value(
|
final child = ChangeNotifierProvider<Node>.value(
|
||||||
value: widget.node,
|
value: node,
|
||||||
child: Consumer<Node>(
|
child: Consumer<Node>(
|
||||||
builder: (_, __, ___) {
|
builder: (_, __, ___) {
|
||||||
AppFlowyEditorLog.editor
|
AppFlowyEditorLog.editor
|
||||||
.debug('node is rebuilding...: type: ${widget.node.type} ');
|
.debug('node is rebuilding...: type: ${node.type} ');
|
||||||
return CompositedTransformTarget(
|
return CompositedTransformTarget(
|
||||||
link: widget.node.layerLink,
|
link: node.layerLink,
|
||||||
child: widget.builder(context),
|
child: builder(context),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return child;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
const errorBlockComponentBuilderKey = 'errorBlockComponentBuilderKey';
|
const errorBlockComponentBuilderKey = 'errorBlockComponentBuilderKey';
|
||||||
|
|
||||||
|
// this value is used to force show the block action.
|
||||||
|
// it is only for test now.
|
||||||
|
bool forceShowBlockAction = false;
|
||||||
|
|
||||||
typedef BlockActionBuilder = Widget Function(
|
typedef BlockActionBuilder = Widget Function(
|
||||||
BlockComponentContext blockComponentContext,
|
BlockComponentContext blockComponentContext,
|
||||||
BlockComponentActionState state,
|
BlockComponentActionState state,
|
||||||
|
|
|
||||||
|
|
@ -498,13 +498,19 @@ class _DesktopSelectionServiceWidgetState
|
||||||
final startRect = blockRect.topLeft;
|
final startRect = blockRect.topLeft;
|
||||||
final endRect = blockRect.bottomLeft;
|
final endRect = blockRect.bottomLeft;
|
||||||
|
|
||||||
final startDistance = (startRect - offset).distance;
|
final renderBox = selectable.context.findRenderObject() as RenderBox;
|
||||||
final endDistance = (endRect - offset).distance;
|
final globalStartRect = renderBox.localToGlobal(startRect);
|
||||||
|
final globalEndRect = renderBox.localToGlobal(endRect);
|
||||||
|
|
||||||
final isCloserToStart = startDistance < endDistance;
|
final topDistance = (globalStartRect - offset).distanceSquared;
|
||||||
|
final bottomDistance = (globalEndRect - offset).distanceSquared;
|
||||||
|
|
||||||
|
final isCloserToStart = topDistance < bottomDistance;
|
||||||
|
|
||||||
|
final dropPath = isCloserToStart ? node?.path : node?.path.next;
|
||||||
|
|
||||||
return DropTargetRenderData(
|
return DropTargetRenderData(
|
||||||
dropTarget: isCloserToStart ? node : node?.next ?? node,
|
dropPath: dropPath ?? node?.path,
|
||||||
cursorNode: node,
|
cursorNode: node,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,27 +119,33 @@ class SelectionGestureInterceptor {
|
||||||
bool Function(DragEndDetails details)? canPanEnd;
|
bool Function(DragEndDetails details)? canPanEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Data returned when calling [renderDropTargetForOffset]
|
/// Data returned when calling [AppFlowySelectionService.getDropTargetRenderData]
|
||||||
///
|
///
|
||||||
/// Includes the [Node] which the drop target is rendered for
|
/// Includes the position (path) which the drop target is rendered for
|
||||||
/// and the [Node] which the cursor is directly hovering over.
|
/// and the [Node] which the cursor is directly hovering over.
|
||||||
///
|
///
|
||||||
class DropTargetRenderData {
|
class DropTargetRenderData {
|
||||||
const DropTargetRenderData({this.dropTarget, this.cursorNode});
|
const DropTargetRenderData({this.dropPath, this.cursorNode});
|
||||||
|
|
||||||
/// The [Node] which the drop is rendered for,
|
/// The path which the drop is rendered for,
|
||||||
/// this is also the [Node] in which any content should be
|
/// this is the position in which any content should be
|
||||||
/// inserted into.
|
/// inserted into.
|
||||||
///
|
///
|
||||||
final Node? dropTarget;
|
final List<int>? dropPath;
|
||||||
|
|
||||||
/// The [Node] which the cursor is directly hovering over,
|
/// The [Node] which the cursor is directly hovering over,
|
||||||
/// this might be the same as [dropTarget] but might also
|
/// this node __might__ be at same position as [dropPath] but might also
|
||||||
/// be another [Node] if the cursor is between two [Node]s.
|
/// be another [Node] depending on distance to top/bottom of the [Node] to the
|
||||||
|
/// cursors offset.
|
||||||
///
|
///
|
||||||
/// This is useful in case you want to cancel or pause the drop
|
/// This is useful in case you want to cancel or pause the drop
|
||||||
/// for specific [Node]s, in case they as example implement their
|
/// for specific [Node]s, in case they as example implement their
|
||||||
/// own drop logic.
|
/// own drop logic.
|
||||||
///
|
///
|
||||||
final Node? cursorNode;
|
final Node? cursorNode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DropTargetRenderData(dropPath: $dropPath, cursorNode: $cursorNode)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue