diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart index bc068a4ac9656..c86b9af663615 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_toolbar_test.dart @@ -408,7 +408,8 @@ void main() { expect(getNodeText(node), link); }); - testWidgets('insert link and clear link name and remove link', (tester) async { + testWidgets('insert link and clear link name and remove link', + (tester) async { const text = 'edit link', link = 'https://test.appflowy.cloud'; await prepareForToolbar(tester, text); @@ -449,5 +450,77 @@ void main() { expect(getNodeText(node), link); expect(getLinkFromNode(node), null); }); + + testWidgets('edit link text with style', (tester) async { + Attributes getAttribute(Node node, Selection selection) { + Attributes attributes = {}; + final ops = node.delta?.whereType() ?? []; + final startOffset = selection.start.offset; + var start = 0; + for (final op in ops) { + if (start > startOffset) break; + final length = op.length; + if (start + length > startOffset) { + attributes = op.attributes ?? {}; + break; + } + start += length; + } + + return attributes; + } + + const text = 'edit text with style', link = 'https://test.appflowy.cloud'; + await prepareForToolbar(tester, text); + final constSelection = + Selection.single(path: [0], startOffset: 0, endOffset: text.length); + + final bold = find.byFlowySvg(FlowySvgs.toolbar_bold_m), + italic = find.byFlowySvg(FlowySvgs.toolbar_inline_italic_m), + underline = find.byFlowySvg(FlowySvgs.toolbar_underline_m); + await tester.tapButton(bold); + await tester.tapButton(italic); + await tester.tapButton(underline); + + Node node = tester.editor.getNodeAtPath([0]); + Attributes attributes = getAttribute(node, constSelection); + expect(attributes, { + AppFlowyRichTextKeys.bold: true, + AppFlowyRichTextKeys.italic: true, + AppFlowyRichTextKeys.underline: true, + }); + + /// tap link button to show CreateLinkMenu + final linkButton = find.byFlowySvg(FlowySvgs.toolbar_link_m); + await tester.tapButton(linkButton); + + /// search for page and select it + final textField = find.descendant( + of: find.byType(LinkCreateMenu), + matching: find.byType(TextFormField), + ); + await tester.enterText(textField, gettingStarted); + await tester.pumpAndSettle(); + await tester.simulateKeyEvent(LogicalKeyboardKey.enter); + await tester.simulateKeyEvent(LogicalKeyboardKey.escape); + + node = tester.editor.getNodeAtPath([0]); + attributes = getAttribute(node, constSelection); + expect(isPageLink(node), true); + expect(getLinkFromNode(node) == link, false); + expect(attributes[AppFlowyRichTextKeys.bold], true); + expect(attributes[AppFlowyRichTextKeys.italic], true); + expect(attributes[AppFlowyRichTextKeys.underline], true); + + /// remove link + await tester.hoverOnWidget(find.byType(LinkHoverTrigger)); + await tester.tapButton(find.byFlowySvg(FlowySvgs.toolbar_link_unlink_m)); + node = tester.editor.getNodeAtPath([0]); + attributes = getAttribute(node, constSelection); + expect(getLinkFromNode(node) == link, false); + expect(attributes[AppFlowyRichTextKeys.bold], true); + expect(attributes[AppFlowyRichTextKeys.italic], true); + expect(attributes[AppFlowyRichTextKeys.underline], true); + }); }); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart index 83f6aa8403413..72e7c83d0ba8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_extension.dart @@ -8,18 +8,13 @@ extension LinkExtension on EditorState { if (node == null) { return; } + final attributes = _getAttribute(node, selection); + attributes[BuiltInAttributeKey.href] = null; + attributes[kIsPageLink] = null; final index = selection.normalized.startIndex; final length = selection.length; final transaction = this.transaction - ..formatText( - node, - index, - length, - { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); + ..formatText(node, index, length, attributes); apply(transaction); } @@ -27,14 +22,52 @@ extension LinkExtension on EditorState { final node = getNodeAtPath(selection.start.path); if (node == null) return; final transaction = this.transaction; + final attributes = _getAttribute(node, selection); + attributes.addAll(info.toAttribute()); final linkName = info.name.isEmpty ? info.link : info.name; transaction.replaceText( node, selection.startIndex, selection.length, linkName, - attributes: info.toAttribute(), + attributes: attributes, ); apply(transaction); } + + void removeAndReplaceLink( + Selection selection, + String text, + ) { + final node = getNodeAtPath(selection.end.path); + if (node == null) { + return; + } + final attributes = _getAttribute(node, selection); + attributes[BuiltInAttributeKey.href] = null; + attributes[kIsPageLink] = null; + final index = selection.normalized.startIndex; + final length = selection.length; + final transaction = this.transaction + ..replaceText(node, index, length, text, attributes: attributes); + apply(transaction); + } + + Attributes _getAttribute(Node node, Selection selection) { + Attributes attributes = {}; + final ops = node.delta?.whereType() ?? []; + final startOffset = selection.start.offset; + var start = 0; + for (final op in ops) { + if (start > startOffset) break; + final length = op.length; + if (start + length > startOffset) { + attributes = op.attributes ?? {}; + break; + } + start += length; + } + + return attributes; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart index 04b9d8ceee5dd..947b18360f0ee 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_hover_menu.dart @@ -189,7 +189,7 @@ class _LinkHoverTriggerState extends State { onRemoveLink: (linkinfo) { final replaceText = linkinfo.name.isEmpty ? linkinfo.link : linkinfo.name; - onRemoveAndReplaceLink(editorState, selection, replaceText); + editorState.removeAndReplaceLink(selection, replaceText); }, ), child: child, @@ -269,31 +269,6 @@ class _LinkHoverTriggerState extends State { ); } } - - void onRemoveAndReplaceLink( - EditorState editorState, - Selection selection, - String text, - ) { - final node = editorState.getNodeAtPath(selection.end.path); - if (node == null) { - return; - } - final index = selection.normalized.startIndex; - final length = selection.length; - final transaction = editorState.transaction - ..replaceText( - node, - index, - length, - text, - attributes: { - BuiltInAttributeKey.href: null, - kIsPageLink: null, - }, - ); - editorState.apply(transaction); - } } class LinkHoverMenu extends StatefulWidget {