Skip to content

Commit

Permalink
feat: add floating toolbar on mobile platform
Browse files Browse the repository at this point in the history
  • Loading branch information
LucasXu0 committed Sep 20, 2023
1 parent 538db46 commit 594d8b3
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 13 deletions.
26 changes: 15 additions & 11 deletions example/lib/pages/mobile_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,20 +52,24 @@ class _MobileEditorState extends State<MobileEditor> {
children: [
// build appflowy editor
Expanded(
child: AppFlowyEditor(
editorStyle: editorStyle,
child: MobileFloatingToolbar(
editorState: editorState,
editorScrollController: editorScrollController,
blockComponentBuilders: blockComponentBuilders,
// showcase 3: customize the header and footer.
header: Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Image.asset(
'assets/images/header.png',
child: AppFlowyEditor(
editorStyle: editorStyle,
editorState: editorState,
editorScrollController: editorScrollController,
blockComponentBuilders: blockComponentBuilders,
// showcase 3: customize the header and footer.
header: Padding(
padding: const EdgeInsets.only(bottom: 10.0),
child: Image.asset(
'assets/images/header.png',
),
),
footer: const SizedBox(
height: 100,
),
),
footer: const SizedBox(
height: 100,
),
),
),
Expand Down
1 change: 1 addition & 0 deletions lib/src/editor/toolbar/mobile/mobile.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'mobile_floating_toolbar/mobile_floating_toolbar.dart';
export 'mobile_toolbar.dart';
export 'mobile_toolbar_item.dart';
export 'mobile_toolbar_style.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';

class MobileFloatingToolbarItem {
const MobileFloatingToolbarItem({

Check warning on line 5 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L5

Added line #L5 was not covered by tests
required this.builder,
});

final WidgetBuilder builder;
}

/// A mobile floating toolbar that displays at the top of the editor when the selection is not collapsed.
/// and will be hidden when the selection is collapsed.
///
/// Normally, it will show copy, cut, paste.
class MobileFloatingToolbar extends StatefulWidget {
const MobileFloatingToolbar({

Check warning on line 17 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L17

Added line #L17 was not covered by tests
super.key,
required this.editorState,
required this.editorScrollController,
required this.child,
});

final EditorState editorState;
final EditorScrollController editorScrollController;
final Widget child;

@override
State<MobileFloatingToolbar> createState() => _MobileFloatingToolbarState();

Check warning on line 29 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L28-L29

Added lines #L28 - L29 were not covered by tests
}

class _MobileFloatingToolbarState extends State<MobileFloatingToolbar>
with WidgetsBindingObserver {
OverlayEntry? _toolbarContainer;

EditorState get editorState => widget.editorState;

Check warning on line 36 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L36

Added line #L36 was not covered by tests

bool _isToolbarVisible = false;
// use for skipping the first build for the toolbar when the selection is collapsed.
Selection? prevSelection;

@override

Check warning on line 42 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L42

Added line #L42 was not covered by tests
void initState() {
super.initState();

Check warning on line 44 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L44

Added line #L44 was not covered by tests

WidgetsBinding.instance.addObserver(this);
editorState.selectionNotifier.addListener(_onSelectionChanged);
widget.editorScrollController.offsetNotifier.addListener(
_onScrollPositionChanged,

Check warning on line 49 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L46-L49

Added lines #L46 - L49 were not covered by tests
);
}

@override

Check warning on line 53 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L53

Added line #L53 was not covered by tests
void didUpdateWidget(MobileFloatingToolbar oldWidget) {
super.didUpdateWidget(oldWidget);

Check warning on line 55 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L55

Added line #L55 was not covered by tests

if (widget.editorState != oldWidget.editorState) {
editorState.selectionNotifier.addListener(_onSelectionChanged);

Check warning on line 58 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L57-L58

Added lines #L57 - L58 were not covered by tests
}
}

@override

Check warning on line 62 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L62

Added line #L62 was not covered by tests
void dispose() {
editorState.selectionNotifier.removeListener(_onSelectionChanged);
widget.editorScrollController.offsetNotifier.removeListener(
_onScrollPositionChanged,

Check warning on line 66 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L64-L66

Added lines #L64 - L66 were not covered by tests
);
WidgetsBinding.instance.removeObserver(this);

Check warning on line 68 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L68

Added line #L68 was not covered by tests

_clear();

Check warning on line 70 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L70

Added line #L70 was not covered by tests

super.dispose();

Check warning on line 72 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L72

Added line #L72 was not covered by tests
}

@override

Check warning on line 75 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L75

Added line #L75 was not covered by tests
void reassemble() {
super.reassemble();

Check warning on line 77 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L77

Added line #L77 was not covered by tests

_clear();

Check warning on line 79 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L79

Added line #L79 was not covered by tests
}

@override

Check warning on line 82 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L82

Added line #L82 was not covered by tests
Widget build(BuildContext context) {
return widget.child;

Check warning on line 84 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L84

Added line #L84 was not covered by tests
}

void _onSelectionChanged() {
final selection = editorState.selection;
final selectionType = editorState.selectionType;
if (selection == null || selectionType == SelectionType.block) {
_clear();
} else if (selection.isCollapsed) {
if (_isToolbarVisible) {
_clear();
} else if (prevSelection == selection) {
_showAfterDelay(const Duration(milliseconds: 400));

Check warning on line 96 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L87-L96

Added lines #L87 - L96 were not covered by tests
}
prevSelection = selection;

Check warning on line 98 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L98

Added line #L98 was not covered by tests
} else {
// uses debounce to avoid the computing the rects too frequently.
_showAfterDelay(const Duration(milliseconds: 400));

Check warning on line 101 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L101

Added line #L101 was not covered by tests
}
}

void _onScrollPositionChanged() {
_clear();

Check warning on line 106 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L105-L106

Added lines #L105 - L106 were not covered by tests
}

final String _debounceKey = 'show the toolbar';
void _clear() {
Debounce.cancel(_debounceKey);

Check warning on line 111 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L110-L111

Added lines #L110 - L111 were not covered by tests

_toolbarContainer?.remove();
_toolbarContainer = null;
_isToolbarVisible = false;
prevSelection = null;

Check warning on line 116 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L113-L116

Added lines #L113 - L116 were not covered by tests
}

void _showAfterDelay([Duration duration = Duration.zero]) {

Check warning on line 119 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L119

Added line #L119 was not covered by tests
// uses debounce to avoid the computing the rects too frequently.
Debounce.debounce(
_debounceKey,

Check warning on line 122 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L121-L122

Added lines #L121 - L122 were not covered by tests
duration,
() {
_clear(); // clear the previous toolbar.
_showToolbar();

Check warning on line 126 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L124-L126

Added lines #L124 - L126 were not covered by tests
},
);
}

void _showToolbar() {
final rects = editorState.selectionRects();
if (rects.isEmpty) {

Check warning on line 133 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L131-L133

Added lines #L131 - L133 were not covered by tests
return;
}

final rect = _findSuitableRect(rects);
_toolbarContainer = OverlayEntry(
builder: (context) {
return _buildToolbar(

Check warning on line 140 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L137-L140

Added lines #L137 - L140 were not covered by tests
context,
rect.topCenter,

Check warning on line 142 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L142

Added line #L142 was not covered by tests
);
},
);
Overlay.of(context).insert(_toolbarContainer!);
_isToolbarVisible = true;

Check warning on line 147 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L146-L147

Added lines #L146 - L147 were not covered by tests
}

Widget _buildToolbar(

Check warning on line 150 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L150

Added line #L150 was not covered by tests
BuildContext context,
Offset? offset,
) {
return AdaptiveTextSelectionToolbar.editable(

Check warning on line 154 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L154

Added line #L154 was not covered by tests
clipboardStatus: ClipboardStatus.pasteable,
onCopy: () => copyCommand.execute(editorState),
onCut: () => cutCommand.execute(editorState),
onPaste: () => pasteCommand.execute(editorState),
onSelectAll: () => selectAllCommand.execute(editorState),
anchors: TextSelectionToolbarAnchors(

Check warning on line 160 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L156-L160

Added lines #L156 - L160 were not covered by tests
primaryAnchor: offset ?? Offset.zero,
),
);
}

Rect _findSuitableRect(Iterable<Rect> rects) {
assert(rects.isNotEmpty);

Check warning on line 167 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L166-L167

Added lines #L166 - L167 were not covered by tests

final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;

Check warning on line 170 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L170

Added line #L170 was not covered by tests

// find the min offset with non-negative dy.
final rectsWithNonNegativeDy = rects.where(
(element) => element.top >= editorOffset.dy,

Check warning on line 174 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L173-L174

Added lines #L173 - L174 were not covered by tests
);
if (rectsWithNonNegativeDy.isEmpty) {

Check warning on line 176 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L176

Added line #L176 was not covered by tests
// if all the rects offset is negative, then the selection is not visible.
return Rect.zero;
}

final minRect = rectsWithNonNegativeDy.reduce((min, current) {
if (min.top < current.top) {

Check warning on line 182 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L181-L182

Added lines #L181 - L182 were not covered by tests
return min;
} else if (min.top == current.top) {
return min.top < current.top ? min : current;

Check warning on line 185 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L184-L185

Added lines #L184 - L185 were not covered by tests
} else {
return current;
}
});

return minRect;
}

(double? left, double top, double? right) calculateToolbarOffset(Rect rect) {

Check warning on line 194 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L194

Added line #L194 was not covered by tests
final editorOffset =
editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
final editorSize = editorState.renderBox?.size ?? Size.zero;
final editorRect = editorOffset & editorSize;
final left = (rect.left - editorOffset.dx).abs();
final right = (rect.right - editorOffset.dx).abs();
final width = editorSize.width;
final threshold = width / 3.0;
final top = rect.top < floatingToolbarHeight
? rect.bottom + floatingToolbarHeight
: rect.top;
if (left <= threshold) {

Check warning on line 206 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L196-L206

Added lines #L196 - L206 were not covered by tests
// show in left
return (rect.left, top, null);
} else if (left >= threshold && right <= threshold * 2.0) {

Check warning on line 209 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L208-L209

Added lines #L208 - L209 were not covered by tests
// show in center
return (threshold, top, null);
} else {
// show in right
return (null, top, editorRect.right - rect.right);

Check warning on line 214 in lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/editor/toolbar/mobile/mobile_floating_toolbar/mobile_floating_toolbar.dart#L214

Added line #L214 was not covered by tests
}
}
}
4 changes: 2 additions & 2 deletions lib/src/editor_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ class EditorState {
blockComponentDelta: delta
.slice(
selection.startIndex,
delta.length,
selection.isSingle ? selection.endIndex : delta.length,
)
.toJson(),
},
Expand All @@ -333,7 +333,7 @@ class EditorState {
node = node.children.last;
}
delta = node.delta;
if (delta != null) {
if (delta != null && !selection.isSingle) {
if (node.parent != null) {
node.insertBefore(
node.copyWith(
Expand Down

0 comments on commit 594d8b3

Please sign in to comment.