chore: fotmat the code and implement floating_toolbar
This commit is contained in:
parent
5f568f674a
commit
e64d829012
12 changed files with 126 additions and 37 deletions
|
|
@ -22,8 +22,10 @@ linter:
|
|||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
- always_declare_return_types
|
||||
- require_trailing_commas
|
||||
- unnecessary_raw_strings
|
||||
- use_decorated_box
|
||||
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ class _HomePageState extends State<HomePage> {
|
|||
editorState: _editorState,
|
||||
);
|
||||
textRobot.insertText(
|
||||
r'''
|
||||
'''
|
||||
Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
||||
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
|
||||
|
||||
|
|
@ -374,6 +374,6 @@ Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC
|
|||
editorStyle,
|
||||
...darkPluginStyleExtension,
|
||||
quote,
|
||||
]);
|
||||
],);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,14 +34,23 @@ class SimpleEditor extends StatelessWidget {
|
|||
..handler = debugPrint
|
||||
..level = LogLevel.all;
|
||||
onEditorStateChange(editorState);
|
||||
final scrollController = ScrollController();
|
||||
if (PlatformExtension.isDesktopOrWeb) {
|
||||
return FloatingToolbar(
|
||||
editorState: editorState,
|
||||
child: _buildEditor(context, editorState));
|
||||
editorState: editorState,
|
||||
scrollController: scrollController,
|
||||
child: _buildEditor(
|
||||
context,
|
||||
editorState,
|
||||
scrollController,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(child: _buildEditor(context, editorState)),
|
||||
Expanded(
|
||||
child: _buildEditor(context, editorState, scrollController),
|
||||
),
|
||||
if (Platform.isIOS || Platform.isAndroid)
|
||||
_buildMobileToolbar(context, editorState),
|
||||
],
|
||||
|
|
@ -56,11 +65,16 @@ class SimpleEditor extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildEditor(BuildContext context, EditorState editorState) {
|
||||
Widget _buildEditor(
|
||||
BuildContext context,
|
||||
EditorState editorState,
|
||||
ScrollController? scrollController,
|
||||
) {
|
||||
return AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
themeData: themeData,
|
||||
autoFocus: editorState.document.isEmpty,
|
||||
scrollController: scrollController,
|
||||
// customBuilders: {
|
||||
// 'paragraph': TextBlockComponentBuilder(),
|
||||
// 'todo_list': TodoListBlockComponentBuilder(),
|
||||
|
|
|
|||
|
|
@ -36,5 +36,5 @@ ThemeData customizeEditorTheme(BuildContext context) {
|
|||
editorStyle,
|
||||
...darkPluginStyleExtension,
|
||||
quote,
|
||||
]);
|
||||
],);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
Future<void> onInsert(
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class _ScrollServiceWidgetState extends State<ScrollServiceWidget>
|
|||
@override
|
||||
void startAutoScroll(
|
||||
Offset offset, {
|
||||
double edgeOffset = 200,
|
||||
double edgeOffset = 100,
|
||||
AxisDirection? direction,
|
||||
}) =>
|
||||
forward.startAutoScroll(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/selection/desktop_selection_service.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/selection/mobile_selection_service.dart';
|
||||
import 'package:appflowy_editor/src/editor/util/util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart' hide Overlay, OverlayEntry;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/editor/editor_component/service/shortcuts/character_shortcut_event.dart';
|
||||
import 'package:appflowy_editor/src/editor/util/util.dart';
|
||||
|
||||
/// Show the slash menu
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
/// A floating toolbar that displays at the top of the editor when the selection
|
||||
/// and will be hidden when the selection is collapsed.
|
||||
|
|
@ -9,10 +10,12 @@ class FloatingToolbar extends StatefulWidget {
|
|||
const FloatingToolbar({
|
||||
super.key,
|
||||
required this.editorState,
|
||||
required this.scrollController,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final EditorState editorState;
|
||||
final ScrollController scrollController;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
|
|
@ -29,17 +32,14 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
|
|||
super.initState();
|
||||
|
||||
editorState.selectionNotifier.addListener(_onSelectionChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
editorState.service.scrollService?.scrollController
|
||||
.addListener(_onScrollPositionChanged);
|
||||
});
|
||||
|
||||
widget.scrollController.addListener(_onScrollPositionChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
editorState.selectionNotifier.removeListener(_onSelectionChanged);
|
||||
editorState.service.scrollService?.scrollController
|
||||
.removeListener(_onScrollPositionChanged);
|
||||
widget.scrollController.removeListener(_onScrollPositionChanged);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
|
@ -54,16 +54,17 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
|
|||
if (selection == null || selection.isCollapsed) {
|
||||
_clear();
|
||||
} else {
|
||||
_show();
|
||||
// uses debounce to avoid the computing the rects too frequently.
|
||||
_showAfter(const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
|
||||
void _onScrollPositionChanged() {
|
||||
final offset = editorState.service.scrollService?.scrollController.offset;
|
||||
if (offset != null) {
|
||||
Log.toolbar.debug('offset = $offset');
|
||||
_show();
|
||||
}
|
||||
final offset = widget.scrollController.offset;
|
||||
Log.toolbar.debug('offset = $offset');
|
||||
|
||||
_clear();
|
||||
_showAfter(Duration.zero);
|
||||
}
|
||||
|
||||
final String _debounceKey = 'show the toolbar';
|
||||
|
|
@ -74,29 +75,75 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
|
|||
_toolbarContainer = null;
|
||||
}
|
||||
|
||||
void _show() {
|
||||
void _showAfter([Duration duration = Duration.zero]) {
|
||||
_clear(); // clear the previous toolbar
|
||||
|
||||
// uses debounce to avoid the computing the rects too frequently.
|
||||
Debounce.debounce(
|
||||
_debounceKey,
|
||||
const Duration(milliseconds: 200),
|
||||
duration,
|
||||
() {
|
||||
final rects = _computeSelectionRects();
|
||||
if (rects.isNotEmpty) {
|
||||
Log.toolbar.debug('rects = $rects');
|
||||
}
|
||||
_showToolbar();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showToolbar() {
|
||||
final rects = _computeSelectionRects();
|
||||
if (rects.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final offset = _findSuitableOffset(rects.map((e) => e.topLeft));
|
||||
_toolbarContainer = OverlayEntry(
|
||||
builder: (context) {
|
||||
return Positioned(
|
||||
left: offset.dx,
|
||||
top: max(0, offset.dy) - 30,
|
||||
child: _buildToolbar(context),
|
||||
);
|
||||
},
|
||||
);
|
||||
Overlay.of(context).insert(_toolbarContainer!);
|
||||
}
|
||||
|
||||
Widget _buildToolbar(BuildContext context) {
|
||||
return Container(
|
||||
width: 300,
|
||||
height: 30,
|
||||
color: Colors.red,
|
||||
);
|
||||
}
|
||||
|
||||
/// Compute the rects of the selection.
|
||||
List<Offset> _computeSelectionRects() {
|
||||
List<Rect> _computeSelectionRects() {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null || selection.isCollapsed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final rects = <Rect>[];
|
||||
for (final node in nodes) {
|
||||
final selectable = node.selectable;
|
||||
if (selectable == null) {
|
||||
continue;
|
||||
}
|
||||
final nodeRects = selectable.getRectsInSelection(selection);
|
||||
if (nodeRects.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final renderBox = node.renderBox;
|
||||
if (renderBox == null) {
|
||||
continue;
|
||||
}
|
||||
for (final rect in nodeRects) {
|
||||
final globalOffset = renderBox.localToGlobal(rect.topLeft);
|
||||
rects.add(globalOffset & rect.size);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
final rects = nodes
|
||||
.map(
|
||||
(node) => node.selectable
|
||||
|
|
@ -109,7 +156,32 @@ class _FloatingToolbarState extends State<FloatingToolbar> {
|
|||
.whereNotNull()
|
||||
.expand((element) => element)
|
||||
.toList();
|
||||
*/
|
||||
|
||||
return rects;
|
||||
}
|
||||
|
||||
Offset _findSuitableOffset(Iterable<Offset> offsets) {
|
||||
assert(offsets.isNotEmpty);
|
||||
|
||||
// find the min offset with non-negative dy.
|
||||
final offsetsWithNonNegativeDy =
|
||||
offsets.where((element) => element.dy >= 30);
|
||||
if (offsetsWithNonNegativeDy.isEmpty) {
|
||||
// if all the rects offset is negative, then the selection is not visible.
|
||||
return offsets.last;
|
||||
}
|
||||
|
||||
final minOffset = offsetsWithNonNegativeDy.reduce((min, current) {
|
||||
if (min.dy < current.dy) {
|
||||
return min;
|
||||
} else if (min.dy == current.dy) {
|
||||
return min.dx < current.dx ? min : current;
|
||||
} else {
|
||||
return current;
|
||||
}
|
||||
});
|
||||
|
||||
return minOffset;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
this.customActionMenuBuilder,
|
||||
this.showDefaultToolbar = true,
|
||||
this.shrinkWrap = false,
|
||||
this.scrollController,
|
||||
ThemeData? themeData,
|
||||
}) : super(key: key) {
|
||||
this.themeData = themeData ??
|
||||
|
|
@ -87,6 +88,8 @@ class AppFlowyEditor extends StatefulWidget {
|
|||
|
||||
late final ThemeData themeData;
|
||||
|
||||
final ScrollController? scrollController;
|
||||
|
||||
@override
|
||||
State<AppFlowyEditor> createState() => _AppFlowyEditorState();
|
||||
}
|
||||
|
|
@ -169,6 +172,7 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
|||
child: _buildScroll(
|
||||
child: ScrollServiceWidget(
|
||||
key: editorState.service.scrollServiceKey,
|
||||
scrollController: widget.scrollController,
|
||||
child: Container(
|
||||
color: editorStyle.backgroundColor,
|
||||
padding: editorStyle.padding!,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ dependencies:
|
|||
flutter_localizations:
|
||||
sdk: flutter
|
||||
tuple: ^2.0.1
|
||||
collection: ^1.17.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
|||
|
|
@ -111,9 +111,9 @@ class TestableEditor {
|
|||
_editorState.document.addParagraph(initialText: '');
|
||||
}
|
||||
|
||||
Future<void> updateSelection(Selection? selection) {
|
||||
Future<void> updateSelection(Selection? selection) async {
|
||||
_editorState.selection = selection;
|
||||
return tester.pumpAndSettle();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Node? nodeAtPath(Path path) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue