TF-4142 Implement hyperlink insertion

This commit is contained in:
dab246
2025-11-10 12:09:19 +07:00
committed by Dat H. Pham
parent db22415a3e
commit 249520b711
14 changed files with 257 additions and 4 deletions
@@ -12,6 +12,7 @@ import 'package:tmail_ui_user/features/base/widget/keyboard/keyboard_handler_wra
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/composer_print_draft_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_edit_recipient_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_insert_link_composer_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_recipients_collapsed_extensions.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/handle_keyboard_shortcut_actions_extension.dart';
import 'package:tmail_ui_user/features/composer/presentation/extensions/mark_as_important_extension.dart';
@@ -248,6 +249,7 @@ class ComposerView extends GetWidget<ComposerController> {
onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged,
height: constraintsEditor.maxHeight,
horizontalPadding: ComposerStyle.mobileEditorHorizontalPadding,
linkOverlayOptions: controller.createLinkOverlayOptions(context),
onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
onDragOver: controller.handleOnDragOverHtmlEditorWeb,
onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction(
@@ -489,6 +491,7 @@ class ComposerView extends GetWidget<ComposerController> {
onEditorTextSizeChanged: controller.richTextWebController?.onEditorTextSizeChanged,
height: constraintsEditor.maxHeight,
horizontalPadding: ComposerStyle.desktopEditorHorizontalPadding,
linkOverlayOptions: controller.createLinkOverlayOptions(context),
onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
onDragOver: controller.handleOnDragOverHtmlEditorWeb,
onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction(
@@ -551,6 +554,7 @@ class ComposerView extends GetWidget<ComposerController> {
toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
onOpenInsertLink: controller.openInsertLink,
)),
],
),
@@ -760,6 +764,7 @@ class ComposerView extends GetWidget<ComposerController> {
onEditorTextSizeChanged: controller.richTextWebController!.onEditorTextSizeChanged,
height: constraintsBody.maxHeight,
horizontalPadding: ComposerStyle.desktopEditorHorizontalPadding,
linkOverlayOptions: controller.createLinkOverlayOptions(context),
onDragEnter: controller.handleOnDragEnterHtmlEditorWeb,
onDragOver: controller.handleOnDragOverHtmlEditorWeb,
onPasteImageSuccessAction: (listFileUpload) => controller.handleOnPasteImageSuccessAction(
@@ -821,6 +826,7 @@ class ComposerView extends GetWidget<ComposerController> {
toggleRequestReadReceiptAction: () => controller.toggleRequestReadReceipt(context),
toggleMarkAsImportantAction: () => controller.toggleMarkAsImportant(context),
saveAsTemplateAction: () => controller.handleClickSaveAsTemplateButton(context),
onOpenInsertLink: controller.openInsertLink,
)),
],
),
@@ -0,0 +1,89 @@
import 'package:core/presentation/extensions/color_extension.dart';
import 'package:core/presentation/utils/theme_utils.dart';
import 'package:core/presentation/views/button/tmail_button_widget.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:html_editor_enhanced/html_editor.dart';
import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
extension HandleInsertLinkComposerExtension on ComposerController {
void openInsertLink() {
richTextWebController?.editorController.openInsertLinkDialog();
}
LinkOverlayOptions createLinkOverlayOptions(BuildContext context) {
final appLocalizations = AppLocalizations.of(context);
return LinkOverlayOptions(
tooltipOptions: LinkTooltipOverlayOptions(
linkPrefixLabel: '${appLocalizations.goTo}:',
editLinkLabel: appLocalizations.change,
removeLinkTooltipMessage: appLocalizations.removeLink,
linkPrefixLabelStyle: ThemeUtils.textStyleBodyBody3(
color: AppColor.gray900,
),
linkLabelStyle: ThemeUtils.textStyleBodyBody3(
color: AppColor.blue700,
),
editLinkLabelStyle: ThemeUtils.textStyleBodyBody3(
color: AppColor.primaryMain,
),
removeLinkButtonBuilder: (_, onTap) => TMailButtonWidget.fromIcon(
icon: imagePaths.icRemoveLink,
iconSize: 20,
iconColor: AppColor.primaryLinShare,
backgroundColor: Colors.transparent,
onTapActionCallback: onTap,
),
),
editDialogOptions: LinkEditDialogOverlayOptions(
hintText: appLocalizations.text,
hintUrl: appLocalizations.typeOrPasteLink,
applyButtonLabel: appLocalizations.apply,
hintTextStyle: ThemeUtils.textStyleBodyBody3(
color: AppColor.steelGray400,
),
inputTextStyle: ThemeUtils.textStyleBodyBody3(
color: AppColor.gray900,
),
applyButtonTextStyle: (isEnabled) => ThemeUtils.textStyleM3LabelLarge(
color: isEnabled ? AppColor.primaryMain : AppColor.gray400,
),
textPrefixIcon: SvgPicture.asset(
imagePaths.icText,
width: 24,
height: 24,
fit: BoxFit.fill,
),
urlPrefixIcon: SvgPicture.asset(
imagePaths.icInsertLink,
width: 24,
height: 24,
fit: BoxFit.fill,
),
backgroundColor: Colors.white,
inputBackgroundColor: Colors.white,
dialogPadding: const EdgeInsetsDirectional.only(
start: 16,
end: 12,
top: 16,
bottom: 16,
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderSide: BorderSide(
width: 1,
color: AppColor.m3Neutral90,
),
),
focusedBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10)),
borderSide: BorderSide(
width: 1,
color: AppColor.primaryMain,
),
),
),
);
}
}
@@ -29,6 +29,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
final OnEditorTextSizeChanged? onEditorTextSizeChanged;
final double? height;
final double? horizontalPadding;
final LinkOverlayOptions? linkOverlayOptions;
final OnDragEnterListener? onDragEnter;
final OnDragOverListener? onDragOver;
final OnPasteImageSuccessAction? onPasteImageSuccessAction;
@@ -51,6 +52,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
this.onEditorTextSizeChanged,
this.height,
this.horizontalPadding,
this.linkOverlayOptions,
this.onDragEnter,
this.onDragOver,
this.onPasteImageSuccessAction,
@@ -82,6 +84,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -113,6 +116,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -146,6 +150,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -185,6 +190,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -219,6 +225,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -243,6 +250,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin {
onEditorTextSizeChanged: onEditorTextSizeChanged,
height: height,
horizontalPadding: horizontalPadding,
linkOverlayOptions: linkOverlayOptions,
onDragEnter: onDragEnter,
onDragOver: onDragOver,
onPasteImageSuccessAction: onPasteImageSuccessAction,
@@ -30,6 +30,7 @@ class BottomBarComposerWidget extends StatelessWidget {
final VoidCallback printDraftAction;
final VoidCallback toggleMarkAsImportantAction;
final VoidCallback saveAsTemplateAction;
final VoidCallback onOpenInsertLink;
final OnMenuChanged? onPopupMenuChanged;
const BottomBarComposerWidget({
@@ -52,6 +53,7 @@ class BottomBarComposerWidget extends StatelessWidget {
required this.printDraftAction,
required this.toggleMarkAsImportantAction,
required this.saveAsTemplateAction,
required this.onOpenInsertLink,
this.onPopupMenuChanged,
});
@@ -110,6 +112,22 @@ class BottomBarComposerWidget extends StatelessWidget {
),
),
const SizedBox(width: BottomBarComposerWidgetStyle.space),
AbsorbPointer(
absorbing: isCodeViewEnabled,
child: TMailButtonWidget.fromIcon(
icon: imagePaths.icInsertLink,
iconColor: isCodeViewEnabled
? BottomBarComposerWidgetStyle.disabledIconColor
: BottomBarComposerWidgetStyle.iconColor,
borderRadius: BottomBarComposerWidgetStyle.iconRadius,
backgroundColor: Colors.transparent,
padding: BottomBarComposerWidgetStyle.iconPadding,
iconSize: BottomBarComposerWidgetStyle.iconSize,
tooltipMessage: AppLocalizations.of(context).insertLink,
onTapActionCallback: onOpenInsertLink,
),
),
const SizedBox(width: BottomBarComposerWidgetStyle.space),
PopupMenuOverlayWidget(
controller: menuMoreOptionController,
iconButton: HighlightSVGIconOnHover(
@@ -39,6 +39,7 @@ class WebEditorWidget extends StatefulWidget {
final OnEditorTextSizeChanged? onEditorTextSizeChanged;
final double? height;
final double? horizontalPadding;
final LinkOverlayOptions? linkOverlayOptions;
final OnDragEnterListener? onDragEnter;
final OnDragOverListener? onDragOver;
final OnPasteImageSuccessAction? onPasteImageSuccessAction;
@@ -60,6 +61,7 @@ class WebEditorWidget extends StatefulWidget {
this.onEditorTextSizeChanged,
this.height,
this.horizontalPadding,
this.linkOverlayOptions,
this.onDragEnter,
this.onDragOver,
this.onPasteImageSuccessAction,
@@ -141,6 +143,9 @@ class _WebEditorState extends State<WebEditorWidget> {
useDefaultFontStyle: true,
customScrollbar: true,
),
useLinkTooltipOverlay: true,
linkOverlayOptions: widget.linkOverlayOptions
?? const LinkOverlayOptions(),
spellCheck: true,
disableDragAndDrop: true,
normalizeHtmlTextWhenDropping: true,
+43 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2025-11-05T13:34:06.612241",
"@@last_modified": "2025-11-10T12:07:59.064737",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -5069,5 +5069,47 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"insertLink": "Insert link",
"@insertLink": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"goTo": "Go to",
"@goTo": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"change": "Change",
"@change": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"removeLink": "Remove link",
"@removeLink": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"text": "Text",
"@text": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"typeOrPasteLink": "Type or paste a link",
"@typeOrPasteLink": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"apply": "Apply",
"@apply": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -5366,4 +5366,53 @@ class AppLocalizations {
name: 'favoriteMailboxDisplayName',
);
}
String get insertLink {
return Intl.message(
'Insert link',
name: 'insertLink',
);
}
String get goTo {
return Intl.message(
'Go to',
name: 'goTo',
);
}
String get change {
return Intl.message(
'Change',
name: 'change',
);
}
String get removeLink {
return Intl.message(
'Remove link',
name: 'removeLink',
);
}
String get text {
return Intl.message(
'Text',
name: 'text',
);
}
String get typeOrPasteLink {
return Intl.message(
'Type or paste a link',
name: 'typeOrPasteLink',
);
}
String get apply {
return Intl.message(
'Apply',
name: 'apply',
);
}
}