feat: implement link menu

This commit is contained in:
Lucas.Xu 2022-08-22 17:34:16 +08:00
parent f943aeacd7
commit 1f90f30274
11 changed files with 498 additions and 230 deletions

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4.3999H4.11111H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77799 4.4V3.2C5.77799 2.88174 5.89506 2.57652 6.10343 2.35147C6.31181 2.12643 6.59442 2 6.88911 2H9.11133C9.40601 2 9.68863 2.12643 9.897 2.35147C10.1054 2.57652 10.2224 2.88174 10.2224 3.2V4.4M11.8891 4.4V12.8C11.8891 13.1183 11.772 13.4235 11.5637 13.6485C11.3553 13.8736 11.0727 14 10.778 14H5.22244C4.92775 14 4.64514 13.8736 4.43676 13.6485C4.22839 13.4235 4.11133 13.1183 4.11133 12.8V4.4H11.8891Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.88867 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.11133 7.3999V10.9999" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -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<TextInsert>();
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);

View File

@ -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<LinkMenu> createState() => _LinkMenuState();
}
class _LinkMenuState extends State<LinkMenu> {
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,
);
}
}

View File

@ -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<String, ToolbarEventHandler>;
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<String> defaultListToolbarEventNames = [
'Text',
'H1',
'H2',
'H3',
];
mixin ToolbarMixin<T extends StatefulWidget> on State<T> {
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<ToolbarWidget> createState() => _ToolbarWidgetState();
}
class _ToolbarWidgetState extends State<ToolbarWidget> 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');
}
}

View File

@ -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<ToolbarItem> 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<TextNode>();
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;
}

View File

@ -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,
),
),
),
);
}
}

View File

@ -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<T extends StatefulWidget> on State<T> {
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<ToolbarItem> items;
@override
State<ToolbarWidget> createState() => _ToolbarWidgetState();
}
class _ToolbarWidgetState extends State<ToolbarWidget> 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),
),
),
),
);
}
}

View File

@ -87,7 +87,9 @@ class _AppFlowyInputState extends State<AppFlowyInput>
@override
void attach(TextEditingValue textEditingValue) {
_textInputConnection ??= TextInput.attach(
if (_textInputConnection == null ||
_textInputConnection!.attached == false) {
_textInputConnection = TextInput.attach(
this,
const TextInputConfiguration(
// TODO: customize
@ -96,6 +98,7 @@ class _AppFlowyInputState extends State<AppFlowyInput>
textCapitalization: TextCapitalization.sentences,
),
);
}
_textInputConnection!
..setEditingState(textEditingValue)

View File

@ -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<FlowyToolbar>
editorState: widget.editorState,
layerLink: layerLink,
offset: offset.translate(0, -37.0),
handlers: const {},
items: defaultToolbarItems,
),
);
Overlay.of(context)?.insert(_toolbarOverlay!);