diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg
new file mode 100644
index 0000000000..b7f242542d
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/delete.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg
new file mode 100644
index 0000000000..5fbcc8d787
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/link.svg
@@ -0,0 +1,4 @@
+
diff --git a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg
index 612e8377b6..279e7ac471 100644
--- a/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg
+++ b/frontend/app_flowy/packages/appflowy_editor/assets/images/toolbar/link.svg
@@ -1,4 +1,4 @@
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart
index 1d7c68ab80..8416485a5a 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart
@@ -6,6 +6,31 @@ import 'package:appflowy_editor/src/document/text_delta.dart';
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
extension TextNodeExtension on TextNode {
+ dynamic getAttributeInSelection(Selection selection, String styleKey) {
+ final ops = delta.whereType();
+ final startOffset =
+ selection.isBackward ? selection.start.offset : selection.end.offset;
+ final endOffset =
+ selection.isBackward ? selection.end.offset : selection.start.offset;
+ var start = 0;
+ for (final op in ops) {
+ if (start >= endOffset) {
+ break;
+ }
+ final length = op.length;
+ if (start < endOffset && start + length > startOffset) {
+ if (op.attributes?.containsKey(styleKey) == true) {
+ return op.attributes![styleKey];
+ }
+ }
+ start += length;
+ }
+ return null;
+ }
+
+ bool allSatisfyLinkInSelection(Selection selection) =>
+ allSatisfyInSelection(StyleKey.href, null, selection);
+
bool allSatisfyBoldInSelection(Selection selection) =>
allSatisfyInSelection(StyleKey.bold, true, selection);
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
new file mode 100644
index 0000000000..3ebf87f4fa
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/link_menu/link_menu.dart
@@ -0,0 +1,137 @@
+import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:flutter/material.dart';
+
+class LinkMenu extends StatefulWidget {
+ const LinkMenu({
+ Key? key,
+ this.linkText,
+ required this.onSubmitted,
+ required this.onCopyLink,
+ required this.onRemoveLink,
+ }) : super(key: key);
+
+ final String? linkText;
+ final void Function(String text) onSubmitted;
+ final VoidCallback onCopyLink;
+ final VoidCallback onRemoveLink;
+
+ @override
+ State createState() => _LinkMenuState();
+}
+
+class _LinkMenuState extends State {
+ final _textEditingController = TextEditingController();
+
+ @override
+ void initState() {
+ super.initState();
+
+ _textEditingController.text = widget.linkText ?? '';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: 350,
+ height: 200,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ boxShadow: [
+ BoxShadow(
+ blurRadius: 5,
+ spreadRadius: 1,
+ color: Colors.black.withOpacity(0.1),
+ ),
+ ],
+ borderRadius: BorderRadius.circular(6.0),
+ ),
+ child: SizedBox(
+ width: 350,
+ height: 200,
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildHeader(),
+ const SizedBox(height: 16.0),
+ _buildInput(),
+ const SizedBox(height: 16.0),
+ _buildIconButton(
+ iconName: 'link',
+ text: 'Copy link',
+ onPressed: widget.onCopyLink,
+ ),
+ _buildIconButton(
+ iconName: 'delete',
+ text: 'Remove link',
+ onPressed: widget.onRemoveLink,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeader() {
+ return const Text(
+ 'Add your link',
+ style: TextStyle(
+ color: Colors.grey,
+ fontWeight: FontWeight.bold,
+ ),
+ );
+ }
+
+ Widget _buildInput() {
+ return TextField(
+ autofocus: true,
+ style: const TextStyle(fontSize: 14.0),
+ textAlign: TextAlign.left,
+ controller: _textEditingController,
+ onSubmitted: widget.onSubmitted,
+ decoration: const InputDecoration(
+ hintText: 'URL',
+ hintStyle: TextStyle(fontSize: 14.0),
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.all(Radius.circular(12.0)),
+ borderSide: BorderSide(color: Color(0xFFBDBDBD)),
+ ),
+ contentPadding: EdgeInsets.all(16.0),
+ isDense: true,
+ ),
+ );
+ }
+
+ Widget _buildIconButton({
+ required String iconName,
+ required String text,
+ required VoidCallback onPressed,
+ }) {
+ return TextButton.icon(
+ icon: FlowySvg(
+ name: iconName,
+ width: 20.0,
+ height: 20.0,
+ ),
+ style: TextButton.styleFrom(
+ minimumSize: const Size.fromHeight(40),
+ padding: EdgeInsets.zero,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ alignment: Alignment.centerLeft,
+ ),
+ label: Text(
+ text,
+ textAlign: TextAlign.left,
+ style: const TextStyle(
+ color: Colors.black,
+ fontSize: 14.0,
+ ),
+ ),
+ onPressed: onPressed,
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart
deleted file mode 100644
index 4c2b621795..0000000000
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection/toolbar_widget.dart
+++ /dev/null
@@ -1,217 +0,0 @@
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
-import 'package:flutter/material.dart';
-
-import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/infra/flowy_svg.dart';
-import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
-
-typedef ToolbarEventHandler = void Function(EditorState editorState);
-
-typedef ToolbarEventHandlers = Map;
-
-ToolbarEventHandlers defaultToolbarEventHandlers = {
- 'bold': (editorState) => formatBold(editorState),
- 'italic': (editorState) => formatItalic(editorState),
- 'strikethrough': (editorState) => formatStrikethrough(editorState),
- 'underline': (editorState) => formatUnderline(editorState),
- 'quote': (editorState) => formatQuote(editorState),
- 'bulleted_list': (editorState) => formatBulletedList(editorState),
- 'highlight': (editorState) => formatHighlight(editorState),
- 'Text': (editorState) => formatText(editorState),
- 'h1': (editorState) => formatHeading(editorState, StyleKey.h1),
- 'h2': (editorState) => formatHeading(editorState, StyleKey.h2),
- 'h3': (editorState) => formatHeading(editorState, StyleKey.h3),
-};
-
-List defaultListToolbarEventNames = [
- 'Text',
- 'H1',
- 'H2',
- 'H3',
-];
-
-mixin ToolbarMixin on State {
- void hide();
-}
-
-class ToolbarWidget extends StatefulWidget {
- const ToolbarWidget({
- Key? key,
- required this.editorState,
- required this.layerLink,
- required this.offset,
- required this.handlers,
- }) : super(key: key);
-
- final EditorState editorState;
- final LayerLink layerLink;
- final Offset offset;
- final ToolbarEventHandlers handlers;
-
- @override
- State createState() => _ToolbarWidgetState();
-}
-
-class _ToolbarWidgetState extends State with ToolbarMixin {
- // final GlobalKey _listToolbarKey = GlobalKey();
-
- final toolbarHeight = 32.0;
- final topPadding = 5.0;
-
- final listToolbarWidth = 60.0;
- final listToolbarHeight = 120.0;
-
- final cornerRadius = 8.0;
-
- OverlayEntry? _listToolbarOverlay;
-
- @override
- Widget build(BuildContext context) {
- return Positioned(
- top: widget.offset.dx,
- left: widget.offset.dy,
- child: CompositedTransformFollower(
- link: widget.layerLink,
- showWhenUnlinked: true,
- offset: widget.offset,
- child: _buildToolbar(context),
- ),
- );
- }
-
- @override
- void hide() {
- _listToolbarOverlay?.remove();
- _listToolbarOverlay = null;
- }
-
- Widget _buildToolbar(BuildContext context) {
- return Material(
- borderRadius: BorderRadius.circular(cornerRadius),
- color: const Color(0xFF333333),
- child: SizedBox(
- height: toolbarHeight,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // _listToolbar(context),
- _centerToolbarIcon('h1', tooltipMessage: 'Heading 1'),
- _centerToolbarIcon('h2', tooltipMessage: 'Heading 2'),
- _centerToolbarIcon('h3', tooltipMessage: 'Heading 3'),
- _centerToolbarIcon('divider', width: 2),
- _centerToolbarIcon('bold', tooltipMessage: 'Bold'),
- _centerToolbarIcon('italic', tooltipMessage: 'Italic'),
- _centerToolbarIcon('strikethrough',
- tooltipMessage: 'Strikethrough'),
- _centerToolbarIcon('underline', tooltipMessage: 'Underline'),
- _centerToolbarIcon('divider', width: 2),
- _centerToolbarIcon('quote', tooltipMessage: 'Quote'),
- // _centerToolbarIcon('number_list'),
- _centerToolbarIcon('bulleted_list',
- tooltipMessage: 'Bulleted List'),
- _centerToolbarIcon('divider', width: 2),
- _centerToolbarIcon('highlight', tooltipMessage: 'Highlight'),
- ],
- ),
- ),
- );
- }
-
- // Widget _listToolbar(BuildContext context) {
- // return _centerToolbarIcon(
- // 'quote',
- // key: _listToolbarKey,
- // width: listToolbarWidth,
- // onTap: () => _onTapListToolbar(context),
- // );
- // }
-
- Widget _centerToolbarIcon(String name,
- {Key? key, String? tooltipMessage, double? width, VoidCallback? onTap}) {
- return Tooltip(
- key: key,
- preferBelow: false,
- message: tooltipMessage ?? '',
- child: MouseRegion(
- cursor: SystemMouseCursors.click,
- child: GestureDetector(
- onTap: onTap ?? () => _onTap(name),
- child: SizedBox.fromSize(
- size:
- Size(toolbarHeight - (width != null ? 20 : 0), toolbarHeight),
- child: Center(
- child: FlowySvg(
- width: width ?? 20,
- name: 'toolbar/$name',
- ),
- ),
- ),
- ),
- ));
- }
-
- // void _onTapListToolbar(BuildContext context) {
- // // TODO: implement more detailed UI.
- // final items = defaultListToolbarEventNames;
- // final renderBox =
- // _listToolbarKey.currentContext?.findRenderObject() as RenderBox;
- // final offset = renderBox
- // .localToGlobal(Offset.zero)
- // .translate(0, toolbarHeight - cornerRadius);
- // final rect = offset & Size(listToolbarWidth, listToolbarHeight);
-
- // _listToolbarOverlay?.remove();
- // _listToolbarOverlay = OverlayEntry(builder: (context) {
- // return Positioned.fromRect(
- // rect: rect,
- // child: Material(
- // borderRadius: BorderRadius.only(
- // bottomLeft: Radius.circular(cornerRadius),
- // bottomRight: Radius.circular(cornerRadius),
- // ),
- // color: const Color(0xFF333333),
- // child: SingleChildScrollView(
- // child: ListView.builder(
- // itemExtent: toolbarHeight,
- // padding: const EdgeInsets.only(bottom: 10.0),
- // shrinkWrap: true,
- // itemCount: items.length,
- // itemBuilder: ((context, index) {
- // return ListTile(
- // contentPadding: const EdgeInsets.only(
- // left: 3.0,
- // right: 3.0,
- // ),
- // minVerticalPadding: 0.0,
- // title: FittedBox(
- // fit: BoxFit.scaleDown,
- // child: Text(
- // items[index],
- // textAlign: TextAlign.center,
- // style: const TextStyle(
- // color: Colors.white,
- // ),
- // ),
- // ),
- // onTap: () {
- // _onTap(items[index]);
- // },
- // );
- // }),
- // ),
- // ),
- // ),
- // );
- // });
- // // TODO: disable scrolling.
- // Overlay.of(context)?.insert(_listToolbarOverlay!);
- // }
-
- void _onTap(String eventName) {
- if (defaultToolbarEventHandlers.containsKey(eventName)) {
- defaultToolbarEventHandlers[eventName]!(widget.editorState);
- return;
- }
- assert(false, 'Could not find the event handler for $eventName');
- }
-}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
new file mode 100644
index 0000000000..49413311d2
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
@@ -0,0 +1,192 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
+import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
+import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+import 'package:flutter/material.dart';
+import 'package:rich_clipboard/rich_clipboard.dart';
+
+typedef ToolbarEventHandler = void Function(
+ EditorState editorState, BuildContext context);
+typedef ToolbarShowValidator = bool Function(EditorState editorState);
+
+class ToolbarItem {
+ ToolbarItem({
+ required this.icon,
+ this.tooltipsMessage = '',
+ required this.validator,
+ required this.handler,
+ });
+
+ final Widget icon;
+ final String tooltipsMessage;
+ final ToolbarShowValidator validator;
+ final ToolbarEventHandler handler;
+
+ factory ToolbarItem.divider() {
+ return ToolbarItem(
+ icon: const FlowySvg(name: 'toolbar/divider'),
+ validator: (editorState) => true,
+ handler: (editorState, context) {},
+ );
+ }
+}
+
+List defaultToolbarItems = [
+ ToolbarItem(
+ tooltipsMessage: 'Heading 1',
+ icon: const FlowySvg(name: 'toolbar/h1'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => formatHeading(editorState, StyleKey.h1),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Heading 2',
+ icon: const FlowySvg(name: 'toolbar/h2'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => formatHeading(editorState, StyleKey.h2),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Heading 3',
+ icon: const FlowySvg(name: 'toolbar/h3'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => formatHeading(editorState, StyleKey.h3),
+ ),
+ ToolbarItem.divider(),
+ ToolbarItem(
+ tooltipsMessage: 'Bold',
+ icon: const FlowySvg(name: 'toolbar/bold'),
+ validator: _showInTextSelection,
+ handler: (editorState, context) => formatBold(editorState),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Italic',
+ icon: const FlowySvg(name: 'toolbar/italic'),
+ validator: _showInTextSelection,
+ handler: (editorState, context) => formatItalic(editorState),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Underline',
+ icon: const FlowySvg(name: 'toolbar/underline'),
+ validator: _showInTextSelection,
+ handler: (editorState, context) => formatUnderline(editorState),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Strikethrough',
+ icon: const FlowySvg(name: 'toolbar/strikethrough'),
+ validator: _showInTextSelection,
+ handler: (editorState, context) => formatStrikethrough(editorState),
+ ),
+ ToolbarItem.divider(),
+ ToolbarItem(
+ tooltipsMessage: 'Quote',
+ icon: const FlowySvg(name: 'toolbar/quote'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => formatQuote(editorState),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Bulleted list',
+ icon: const FlowySvg(name: 'toolbar/bulleted_list'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => formatBulletedList(editorState),
+ ),
+ ToolbarItem.divider(),
+ ToolbarItem(
+ tooltipsMessage: 'Link',
+ icon: const FlowySvg(name: 'toolbar/link'),
+ validator: _onlyShowInSingleTextSelection,
+ handler: (editorState, context) => _showLinkMenu(editorState, context),
+ ),
+ ToolbarItem(
+ tooltipsMessage: 'Highlight',
+ icon: const FlowySvg(name: 'toolbar/highlight'),
+ validator: _showInTextSelection,
+ handler: (editorState, context) => formatHighlight(editorState),
+ ),
+];
+
+ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) {
+ final nodes = editorState.service.selectionService.currentSelectedNodes;
+ return (nodes.length == 1 && nodes.first is TextNode);
+};
+
+ToolbarShowValidator _showInTextSelection = (editorState) {
+ final nodes = editorState.service.selectionService.currentSelectedNodes
+ .whereType();
+ return nodes.isNotEmpty;
+};
+
+OverlayEntry? _linkMenuOverlay;
+EditorState? _editorState;
+void _showLinkMenu(EditorState editorState, BuildContext context) {
+ _editorState = editorState;
+
+ final rects = editorState.service.selectionService.selectionRects;
+ var maxBottom = 0.0;
+ late Rect matchRect;
+ for (final rect in rects) {
+ if (rect.bottom > maxBottom) {
+ maxBottom = rect.bottom;
+ matchRect = rect;
+ }
+ }
+
+ _dismissLinkMenu();
+
+ // Since the link menu will only show in single text selection,
+ // We get the text node directly instead of judging details again.
+ final selection =
+ editorState.service.selectionService.currentSelection.value!;
+ final index =
+ selection.isBackward ? selection.start.offset : selection.end.offset;
+ final length = (selection.start.offset - selection.end.offset).abs();
+ final node = editorState.service.selectionService.currentSelectedNodes.first
+ as TextNode;
+ final linkText = node.getAttributeInSelection(selection, StyleKey.href);
+ _linkMenuOverlay = OverlayEntry(builder: (context) {
+ return Positioned(
+ top: matchRect.bottom,
+ left: matchRect.left,
+ child: Material(
+ child: LinkMenu(
+ linkText: linkText,
+ onSubmitted: (text) {
+ TransactionBuilder(editorState)
+ ..formatText(node, index, length, {
+ StyleKey.href: text,
+ })
+ ..commit();
+ _dismissLinkMenu();
+ },
+ onCopyLink: () {
+ RichClipboard.setData(RichClipboardData(text: linkText));
+ _dismissLinkMenu();
+ },
+ onRemoveLink: () {
+ TransactionBuilder(editorState)
+ ..formatText(node, index, length, {
+ StyleKey.href: null,
+ })
+ ..commit();
+ _dismissLinkMenu();
+ },
+ ),
+ ),
+ );
+ });
+ Overlay.of(context)?.insert(_linkMenuOverlay!);
+
+ editorState.service.scrollService?.disable();
+ editorState.service.selectionService.currentSelection
+ .addListener(_dismissLinkMenu);
+}
+
+void _dismissLinkMenu() {
+ _linkMenuOverlay?.remove();
+ _linkMenuOverlay = null;
+
+ _editorState?.service.scrollService?.enable();
+ _editorState?.service.selectionService.currentSelection
+ .removeListener(_dismissLinkMenu);
+ _editorState = null;
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart
new file mode 100644
index 0000000000..ce89eef126
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item_widget.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+import 'toolbar_item.dart';
+
+class ToolbarItemWidget extends StatelessWidget {
+ const ToolbarItemWidget({
+ Key? key,
+ required this.item,
+ required this.onPressed,
+ }) : super(key: key);
+
+ final ToolbarItem item;
+ final VoidCallback onPressed;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ width: 28,
+ height: 28,
+ child: Tooltip(
+ preferBelow: false,
+ message: item.tooltipsMessage,
+ child: MouseRegion(
+ cursor: SystemMouseCursors.click,
+ child: IconButton(
+ padding: EdgeInsets.zero,
+ icon: item.icon,
+ iconSize: 28,
+ onPressed: onPressed,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
new file mode 100644
index 0000000000..167f8e79ba
--- /dev/null
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_widget.dart
@@ -0,0 +1,82 @@
+import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
+import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
+import 'package:flutter/material.dart';
+
+import 'package:appflowy_editor/src/editor_state.dart';
+
+mixin ToolbarMixin on State {
+ void hide();
+}
+
+class ToolbarWidget extends StatefulWidget {
+ const ToolbarWidget({
+ Key? key,
+ required this.editorState,
+ required this.layerLink,
+ required this.offset,
+ required this.items,
+ }) : super(key: key);
+
+ final EditorState editorState;
+ final LayerLink layerLink;
+ final Offset offset;
+ final List items;
+
+ @override
+ State createState() => _ToolbarWidgetState();
+}
+
+class _ToolbarWidgetState extends State with ToolbarMixin {
+ OverlayEntry? _listToolbarOverlay;
+
+ @override
+ Widget build(BuildContext context) {
+ return Positioned(
+ top: widget.offset.dx,
+ left: widget.offset.dy,
+ child: CompositedTransformFollower(
+ link: widget.layerLink,
+ showWhenUnlinked: true,
+ offset: widget.offset,
+ child: _buildToolbar(context),
+ ),
+ );
+ }
+
+ @override
+ void hide() {
+ _listToolbarOverlay?.remove();
+ _listToolbarOverlay = null;
+ }
+
+ Widget _buildToolbar(BuildContext context) {
+ final items = widget.items.where(
+ (item) => item.validator(widget.editorState),
+ );
+ return Material(
+ borderRadius: BorderRadius.circular(8.0),
+ color: const Color(0xFF333333),
+ child: Padding(
+ padding: const EdgeInsets.only(left: 8.0, right: 8.0),
+ child: SizedBox(
+ height: 32.0,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: items
+ .map(
+ (item) => Center(
+ child: ToolbarItemWidget(
+ item: item,
+ onPressed: () {
+ item.handler(widget.editorState, context);
+ },
+ ),
+ ),
+ )
+ .toList(growable: false),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
index 9aae2b5fcb..a92fae1b95 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/input_service.dart
@@ -87,15 +87,18 @@ class _AppFlowyInputState extends State
@override
void attach(TextEditingValue textEditingValue) {
- _textInputConnection ??= TextInput.attach(
- this,
- const TextInputConfiguration(
- // TODO: customize
- enableDeltaModel: true,
- inputType: TextInputType.multiline,
- textCapitalization: TextCapitalization.sentences,
- ),
- );
+ if (_textInputConnection == null ||
+ _textInputConnection!.attached == false) {
+ _textInputConnection = TextInput.attach(
+ this,
+ const TextInputConfiguration(
+ // TODO: customize
+ enableDeltaModel: true,
+ inputType: TextInputType.multiline,
+ textCapitalization: TextCapitalization.sentences,
+ ),
+ );
+ }
_textInputConnection!
..setEditingState(textEditingValue)
diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
index bf380290f9..fe4a2beace 100644
--- a/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
+++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
@@ -1,7 +1,8 @@
+import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
import 'package:flutter/material.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/selection/toolbar_widget.dart';
+import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
abstract class FlowyToolbarService {
@@ -41,7 +42,7 @@ class _FlowyToolbarState extends State
editorState: widget.editorState,
layerLink: layerLink,
offset: offset.translate(0, -37.0),
- handlers: const {},
+ items: defaultToolbarItems,
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);