mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: improve editing link user experience (#3990)
This commit is contained in:
@ -15,7 +15,6 @@ List<MobileToolbarItem> getMobileToolbarItems() {
|
||||
buildTextAndBackgroundColorMobileToolbarItem(),
|
||||
mobileAddBlockToolbarItem,
|
||||
mobileConvertBlockToolbarItem,
|
||||
linkMobileToolbarItem,
|
||||
imageMobileToolbarItem,
|
||||
mobileAlignToolbarItem,
|
||||
mobileIndentToolbarItem,
|
||||
|
@ -1,19 +1,25 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final customTextDecorationMobileToolbarItem = MobileToolbarItem.withMenu(
|
||||
itemIconBuilder: (_, __, ___) => const FlowySvg(
|
||||
FlowySvgs.text_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
itemMenuBuilder: (_, editorState, __) {
|
||||
itemMenuBuilder: (_, editorState, service) {
|
||||
final selection = editorState.selection;
|
||||
if (selection == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return _TextDecorationMenu(editorState, selection);
|
||||
return _TextDecorationMenu(
|
||||
editorState,
|
||||
selection,
|
||||
service,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -21,16 +27,20 @@ class _TextDecorationMenu extends StatefulWidget {
|
||||
const _TextDecorationMenu(
|
||||
this.editorState,
|
||||
this.selection,
|
||||
this.service,
|
||||
);
|
||||
|
||||
final EditorState editorState;
|
||||
final Selection selection;
|
||||
final MobileToolbarWidgetService service;
|
||||
|
||||
@override
|
||||
State<_TextDecorationMenu> createState() => _TextDecorationMenuState();
|
||||
}
|
||||
|
||||
class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
EditorState get editorState => widget.editorState;
|
||||
|
||||
final textDecorations = [
|
||||
// BIUS
|
||||
TextDecorationUnit(
|
||||
@ -60,41 +70,90 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
label: AppFlowyEditorL10n.current.embedCode,
|
||||
name: AppFlowyRichTextKeys.code,
|
||||
),
|
||||
|
||||
// link
|
||||
TextDecorationUnit(
|
||||
icon: AFMobileIcons.link,
|
||||
label: AppFlowyEditorL10n.current.link,
|
||||
name: AppFlowyRichTextKeys.href,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bius = textDecorations.map((currentDecoration) {
|
||||
// Check current decoration is active or not
|
||||
final selection = widget.selection;
|
||||
final nodes = widget.editorState.getNodesInSelection(selection);
|
||||
final bool isSelected;
|
||||
if (selection.isCollapsed) {
|
||||
isSelected = widget.editorState.toggledStyle.containsKey(
|
||||
currentDecoration.name,
|
||||
);
|
||||
} else {
|
||||
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
|
||||
return delta.everyAttributes(
|
||||
(attributes) => attributes[currentDecoration.name] == true,
|
||||
);
|
||||
});
|
||||
}
|
||||
final children = textDecorations
|
||||
.map((currentDecoration) {
|
||||
// Check current decoration is active or not
|
||||
final selection = widget.selection;
|
||||
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: AFMobileIcon(
|
||||
afMobileIcons: currentDecoration.icon,
|
||||
color: MobileToolbarTheme.of(context).iconColor,
|
||||
),
|
||||
label: FlowyText(currentDecoration.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
widget.editorState.toggleAttribute(currentDecoration.name);
|
||||
});
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
// only show edit link bottom sheet when selection is not collapsed
|
||||
if (selection.isCollapsed &&
|
||||
currentDecoration.name == AppFlowyRichTextKeys.href) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final bool isSelected;
|
||||
if (selection.isCollapsed) {
|
||||
isSelected = editorState.toggledStyle.containsKey(
|
||||
currentDecoration.name,
|
||||
);
|
||||
} else {
|
||||
isSelected = nodes.allSatisfyInSelection(selection, (delta) {
|
||||
return delta.everyAttributes(
|
||||
(attributes) => attributes[currentDecoration.name] == true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return MobileToolbarItemMenuBtn(
|
||||
icon: AFMobileIcon(
|
||||
afMobileIcons: currentDecoration.icon,
|
||||
color: MobileToolbarTheme.of(context).iconColor,
|
||||
),
|
||||
label: FlowyText(currentDecoration.label),
|
||||
isSelected: isSelected,
|
||||
onPressed: () {
|
||||
if (currentDecoration.name == AppFlowyRichTextKeys.href) {
|
||||
if (selection.isCollapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_closeKeyboard();
|
||||
|
||||
// show edit link bottom sheet
|
||||
final context = nodes.firstOrNull?.context;
|
||||
if (context != null) {
|
||||
final text = editorState
|
||||
.getTextInSelection(
|
||||
widget.selection,
|
||||
)
|
||||
.join('');
|
||||
final href =
|
||||
editorState.getDeltaAttributeValueInSelection<String>(
|
||||
AppFlowyRichTextKeys.href,
|
||||
widget.selection,
|
||||
);
|
||||
showEditLinkBottomSheet(
|
||||
context,
|
||||
text,
|
||||
href,
|
||||
(context, newText, newHref) {
|
||||
_updateTextAndHref(text, href, newText, newHref);
|
||||
context.pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
editorState.toggleAttribute(currentDecoration.name);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.nonNulls
|
||||
.toList();
|
||||
|
||||
return GridView.count(
|
||||
shrinkWrap: true,
|
||||
@ -103,7 +162,49 @@ class _TextDecorationMenuState extends State<_TextDecorationMenu> {
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
childAspectRatio: 4,
|
||||
children: bius,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
void _closeKeyboard() {
|
||||
editorState.updateSelectionWithReason(
|
||||
widget.selection,
|
||||
extraInfo: {
|
||||
disableMobileToolbarKey: true,
|
||||
},
|
||||
);
|
||||
editorState.service.keyboardService?.closeKeyboard();
|
||||
}
|
||||
|
||||
void _updateTextAndHref(
|
||||
String prevText,
|
||||
String? prevHref,
|
||||
String text,
|
||||
String href,
|
||||
) async {
|
||||
final selection = widget.selection;
|
||||
if (!selection.isSingle) {
|
||||
return;
|
||||
}
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = editorState.transaction;
|
||||
if (prevText != text) {
|
||||
transaction.replaceText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
text,
|
||||
);
|
||||
}
|
||||
// if the text is empty, it means the user wants to remove the text
|
||||
if (text.isNotEmpty && prevHref != href) {
|
||||
transaction.formatText(node, selection.startIndex, text.length, {
|
||||
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
|
||||
});
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
void showEditLinkBottomSheet(
|
||||
BuildContext context,
|
||||
String text,
|
||||
String? href,
|
||||
void Function(BuildContext context, String text, String href) onEdit,
|
||||
) {
|
||||
assert(text.isNotEmpty);
|
||||
showFlowyMobileBottomSheet(
|
||||
context,
|
||||
title: LocaleKeys.editor_editLink.tr(),
|
||||
builder: (context) {
|
||||
return MobileBottomSheetEditLinkWidget(
|
||||
text: text,
|
||||
href: href,
|
||||
onEdit: (text, href) => onEdit(context, text, href),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/util/google_font_family_extension.dart';
|
||||
@ -10,6 +11,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
|
||||
class EditorStyleCustomizer {
|
||||
@ -276,7 +278,23 @@ class EditorStyleCustomizer {
|
||||
text: text.text,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
safeLaunchUrl(href);
|
||||
showEditLinkBottomSheet(
|
||||
context,
|
||||
text.text,
|
||||
href,
|
||||
(linkContext, newText, newHref) {
|
||||
_updateTextAndHref(
|
||||
context,
|
||||
node,
|
||||
index,
|
||||
text.text,
|
||||
href,
|
||||
newText,
|
||||
newHref,
|
||||
);
|
||||
linkContext.pop();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -289,4 +307,37 @@ class EditorStyleCustomizer {
|
||||
textSpan,
|
||||
);
|
||||
}
|
||||
|
||||
void _updateTextAndHref(
|
||||
BuildContext context,
|
||||
Node node,
|
||||
int index,
|
||||
String prevText,
|
||||
String? prevHref,
|
||||
String text,
|
||||
String href,
|
||||
) async {
|
||||
final selection = Selection.single(
|
||||
path: node.path,
|
||||
startOffset: index,
|
||||
endOffset: index + prevText.length,
|
||||
);
|
||||
final editorState = context.read<EditorState>();
|
||||
final transaction = editorState.transaction;
|
||||
if (prevText != text) {
|
||||
transaction.replaceText(
|
||||
node,
|
||||
selection.startIndex,
|
||||
selection.length,
|
||||
text,
|
||||
);
|
||||
}
|
||||
// if the text is empty, it means the user wants to remove the text
|
||||
if (text.isNotEmpty && prevHref != href) {
|
||||
transaction.formatText(node, selection.startIndex, text.length, {
|
||||
AppFlowyRichTextKeys.href: href.isEmpty ? null : href,
|
||||
});
|
||||
}
|
||||
await editorState.apply(transaction);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user