From 50af5135c7a0d9251f9239e72d8d3fba402d5e4e Mon Sep 17 00:00:00 2001 From: hieubt Date: Wed, 10 Apr 2024 11:39:28 +0700 Subject: [PATCH] TF-2426 Add insert link dialog to composer web --- .../views/text/text_form_field_builder.dart | 37 +++- .../presentation/composer_controller.dart | 22 ++ .../presentation/composer_view_web.dart | 3 + ...t => insert_link_dialog_widget_style.dart} | 2 +- .../web/insert_link_dialog_builder.dart | 146 ------------- .../web/insert_link_dialog_widget.dart | 194 ++++++++++++++++++ pubspec.lock | 4 +- pubspec.yaml | 2 +- 8 files changed, 259 insertions(+), 151 deletions(-) rename lib/features/composer/presentation/styles/web/{insert_link_dialog_builder_style.dart => insert_link_dialog_widget_style.dart} (98%) delete mode 100644 lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart create mode 100644 lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart index 1d6407e30d..a542d61a21 100644 --- a/core/lib/presentation/views/text/text_form_field_builder.dart +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -79,6 +79,42 @@ class _TextFieldFormBuilderState extends State { @override Widget build(BuildContext context) { + if (widget.validator != null) { + return TextFormField( + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + autofillHints: widget.autofillHints, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onFieldSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + validator: widget.validator, + ); + } return TextField( key: widget.key, controller: _controller, @@ -111,7 +147,6 @@ class _TextFieldFormBuilderState extends State { }, onSubmitted: widget.onTextSubmitted, onTap: widget.onTap, - validator: widget.validator, ); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index ab5a517352..ba35661942 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -59,6 +59,7 @@ import 'package:tmail_ui_user/features/composer/presentation/styles/composer_sty import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/from_composer_bottom_sheet_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/saving_message_dialog_view.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/sending_message_dialog_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart'; import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; @@ -2167,4 +2168,25 @@ class ComposerController extends BaseController with DragDropFileMixin { ccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; bccRecipientState.value = isEnabled ? PrefixRecipientState.disabled : PrefixRecipientState.enabled; } + + void onEditLinkAction( + BuildContext context, + String? text, + String? url, + bool? isOpenNewTab, + String linkTagId + ) async { + Get.dialog( + PointerInterceptor( + child: InsertLinkDialogWidget( + responsiveUtils: responsiveUtils, + editorController: richTextWebController?.editorController, + linkTagId: linkTagId, + displayText: text ?? url ?? '', + link: url ?? '', + openNewTab: isOpenNewTab ?? true, + ) + ) + ); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index efe72a1680..b4e60ae232 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -191,6 +191,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), )), ), ), @@ -432,6 +433,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), ); }), ), @@ -694,6 +696,7 @@ class ComposerView extends GetWidget { width: constraints.maxWidth, height: constraints.maxHeight, onDragEnter: controller.handleOnDragEnterHtmlEditorWeb, + onEditLink: (text, url, isOpenNewTab, linkTagId) => controller.onEditLinkAction(context, text, url, isOpenNewTab, linkTagId), )), ), ), diff --git a/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart similarity index 98% rename from lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart rename to lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart index 65d22693a1..4ff7f09b41 100644 --- a/lib/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart +++ b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; -class InsertLinkDialogBuilderStyle { +class InsertLinkDialogWidgetStyle { static const double actionOverFlowButtonSpacing = 8.0; static const double elevation = 10.0; static const double widthRatio = 0.3; diff --git a/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart b/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart deleted file mode 100644 index df25a569c9..0000000000 --- a/lib/features/composer/presentation/widgets/web/insert_link_dialog_builder.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; -import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/presentation/views/text/text_form_field_builder.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/styles/web/insert_link_dialog_builder_style.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnOpenNewTabChanged = void Function(bool? isOpenNewTab); - -class InsertLinkDialogBuilder { - final BuildContext context; - final ResponsiveUtils responsiveUtils; - final GlobalKey formKey; - final TextEditingController displayTextFieldController; - final TextEditingController linkTextFieldController; - final bool isOpenNewTab; - final FocusNode displayTextFieldFocusNode; - final FocusNode linkTextFieldFocusNode; - final FocusNode openNewTabFocusNode; - final VoidCallback? cancelActionCallback; - final VoidCallback? insertActionCallback; - final OnOpenNewTabChanged? onOpenNewTabChanged; - - const InsertLinkDialogBuilder({ - required this.context, - required this.responsiveUtils, - required this.formKey, - required this.displayTextFieldController, - required this.linkTextFieldController, - required this.isOpenNewTab, - required this.displayTextFieldFocusNode, - required this.linkTextFieldFocusNode, - required this.openNewTabFocusNode, - this.cancelActionCallback, - this.insertActionCallback, - this.onOpenNewTabChanged, - }); - - Future show() async { - return Get.dialog( - PointerInterceptor( - child: AlertDialog( - surfaceTintColor: Colors.white, - title: Text( - AppLocalizations.of(context).insertLink, - textAlign: TextAlign.center, - style: InsertLinkDialogBuilderStyle.tittleStyle - ), - titlePadding: InsertLinkDialogBuilderStyle.tittlePadding, - contentPadding: InsertLinkDialogBuilderStyle.contentPadding, - actionsPadding: InsertLinkDialogBuilderStyle.actionsPadding, - actionsAlignment: MainAxisAlignment.center, - actionsOverflowButtonSpacing: InsertLinkDialogBuilderStyle.actionOverFlowButtonSpacing, - shape: InsertLinkDialogBuilderStyle.shape, - scrollable: true, - elevation: InsertLinkDialogBuilderStyle.elevation, - content: SizedBox( - width: responsiveUtils.getSizeScreenWidth(context) * InsertLinkDialogBuilderStyle.widthRatio, - child: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - AppLocalizations.of(context).textToDisplay, - style: InsertLinkDialogBuilderStyle.fieldTitleStyle, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), - TextFieldBuilder( - controller: displayTextFieldController, - focusNode: displayTextFieldFocusNode, - textInputAction: TextInputAction.next, - maxLines: InsertLinkDialogBuilderStyle.maxLines, - textStyle: InsertLinkDialogBuilderStyle.textInputStyle, - decoration: InputDecoration( - filled: true, - fillColor: Colors.white, - contentPadding: InsertLinkDialogBuilderStyle.textInputContentPadding, - enabledBorder: InsertLinkDialogBuilderStyle.border, - border: InsertLinkDialogBuilderStyle.border, - focusedBorder: InsertLinkDialogBuilderStyle.focusedBorder, - hintText: AppLocalizations.of(context).textToDisplay, - hintStyle: InsertLinkDialogBuilderStyle.hintTextStyle, - ), - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), - Text( - AppLocalizations.of(context).toWhatURLShouldThisLinkGo, - style: InsertLinkDialogBuilderStyle.fieldTitleStyle, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.tittleToFieldSpace), - TextFormFieldBuilder( - controller: linkTextFieldController, - focusNode: linkTextFieldFocusNode, - textInputAction: TextInputAction.done, - textStyle: InsertLinkDialogBuilderStyle.textInputStyle, - decoration: InsertLinkDialogBuilderStyle.urlFieldDecoration, - validator: (String? value) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context).pleaseEnterURL; - } - return null; - }, - ), - const SizedBox(height: InsertLinkDialogBuilderStyle.fieldToFieldSpace), - LabeledCheckbox( - label: AppLocalizations.of(context).openInNewTab, - value: isOpenNewTab, - onChanged: onOpenNewTabChanged, - focusNode: openNewTabFocusNode, - contentPadding: EdgeInsets.zero, - activeColor: AppColor.primaryColor, - ) - ], - ), - ), - ), - actions: [ - buildButtonWrapText( - AppLocalizations.of(context).cancel, - radius: InsertLinkDialogBuilderStyle.buttonRadius, - height: InsertLinkDialogBuilderStyle.buttonHeight, - bgColor: AppColor.colorBgSearchBar, - textStyle: InsertLinkDialogBuilderStyle.buttonCancelTextStyle, - onTap: cancelActionCallback, - ), - buildButtonWrapText( - AppLocalizations.of(context).insert, - radius: InsertLinkDialogBuilderStyle.buttonRadius, - height: InsertLinkDialogBuilderStyle.buttonHeight, - textStyle: InsertLinkDialogBuilderStyle.buttonInsertTextStyle, - bgColor: AppColor.primaryColor, - onTap: insertActionCallback, - ), - ], - ) - ) - ); - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart b/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart new file mode 100644 index 0000000000..819700e34c --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/insert_link_dialog_widget.dart @@ -0,0 +1,194 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class InsertLinkDialogWidget extends StatefulWidget { + final ResponsiveUtils responsiveUtils; + final HtmlEditorController? editorController; + final String linkTagId; + final String displayText; + final String link; + final bool openNewTab; + + const InsertLinkDialogWidget({ + super.key, + required this.responsiveUtils, + required this.editorController, + required this.linkTagId, + required this.displayText, + required this.link, + required this.openNewTab, + }); + + @override + State createState() => _InsertLinkDialogState(); +} + +class _InsertLinkDialogState extends State { + late FocusNode _displayTextFieldFocusNode; + late FocusNode _linkTextFieldFocusNode; + late FocusNode _openNewTabFocusNode; + late TextEditingController _displayTextFieldController; + late TextEditingController _linkTextFieldController; + late HtmlToolbarOptions _htmlToolbarOptions; + late GlobalKey _formKey; + late bool _openNewTab; + + @override + void initState() { + super.initState(); + _displayTextFieldFocusNode = FocusNode(); + _linkTextFieldFocusNode = FocusNode(); + _openNewTabFocusNode = FocusNode(); + _htmlToolbarOptions = const HtmlToolbarOptions(); + _formKey = GlobalKey(); + _displayTextFieldController = TextEditingController(text: widget.displayText); + _linkTextFieldController = TextEditingController(text: widget.link); + _openNewTab = widget.openNewTab; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + surfaceTintColor: Colors.white, + title: Text( + AppLocalizations.of(context).insertLink, + textAlign: TextAlign.center, + style: InsertLinkDialogWidgetStyle.tittleStyle + ), + titlePadding: InsertLinkDialogWidgetStyle.tittlePadding, + contentPadding: InsertLinkDialogWidgetStyle.contentPadding, + actionsPadding: InsertLinkDialogWidgetStyle.actionsPadding, + actionsAlignment: MainAxisAlignment.center, + actionsOverflowButtonSpacing: InsertLinkDialogWidgetStyle.actionOverFlowButtonSpacing, + shape: InsertLinkDialogWidgetStyle.shape, + scrollable: true, + elevation: InsertLinkDialogWidgetStyle.elevation, + content: SizedBox( + width: widget.responsiveUtils.getSizeScreenWidth(context) * InsertLinkDialogWidgetStyle.widthRatio, + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).textToDisplay, + style: InsertLinkDialogWidgetStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.tittleToFieldSpace), + TextFieldBuilder( + controller: _displayTextFieldController, + focusNode: _displayTextFieldFocusNode, + textInputAction: TextInputAction.next, + maxLines: InsertLinkDialogWidgetStyle.maxLines, + textStyle: InsertLinkDialogWidgetStyle.textInputStyle, + decoration: InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: InsertLinkDialogWidgetStyle.textInputContentPadding, + enabledBorder: InsertLinkDialogWidgetStyle.border, + border: InsertLinkDialogWidgetStyle.border, + focusedBorder: InsertLinkDialogWidgetStyle.focusedBorder, + hintText: AppLocalizations.of(context).textToDisplay, + hintStyle: InsertLinkDialogWidgetStyle.hintTextStyle, + ), + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.fieldToFieldSpace), + Text( + AppLocalizations.of(context).toWhatURLShouldThisLinkGo, + style: InsertLinkDialogWidgetStyle.fieldTitleStyle, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.tittleToFieldSpace), + TextFormFieldBuilder( + controller: _linkTextFieldController, + focusNode: _linkTextFieldFocusNode, + textInputAction: TextInputAction.done, + textStyle: InsertLinkDialogWidgetStyle.textInputStyle, + decoration: InsertLinkDialogWidgetStyle.urlFieldDecoration, + validator: (String? value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context).pleaseEnterURL; + } + return null; + }, + ), + const SizedBox(height: InsertLinkDialogWidgetStyle.fieldToFieldSpace), + LabeledCheckbox( + label: AppLocalizations.of(context).openInNewTab, + value: _openNewTab, + onChanged: (value) { + if (value != null) { + setState(() { + _openNewTab = value; + }); + } + }, + focusNode: _openNewTabFocusNode, + contentPadding: EdgeInsets.zero, + activeColor: AppColor.primaryColor, + ) + ], + ), + ), + ), + actions: [ + buildButtonWrapText( + AppLocalizations.of(context).cancel, + radius: InsertLinkDialogWidgetStyle.buttonRadius, + height: InsertLinkDialogWidgetStyle.buttonHeight, + bgColor: AppColor.colorBgSearchBar, + textStyle: InsertLinkDialogWidgetStyle.buttonCancelTextStyle, + onTap: () => Get.back(), + ), + buildButtonWrapText( + AppLocalizations.of(context).insert, + radius: InsertLinkDialogWidgetStyle.buttonRadius, + height: InsertLinkDialogWidgetStyle.buttonHeight, + textStyle: InsertLinkDialogWidgetStyle.buttonInsertTextStyle, + bgColor: AppColor.primaryColor, + onTap: () async { + if (_formKey.currentState != null && _formKey.currentState!.validate()) { + var proceed = await _htmlToolbarOptions.linkInsertInterceptor?.call( + _displayTextFieldController.text.isEmpty + ? _linkTextFieldController.text + : _displayTextFieldController.text, + _linkTextFieldController.text, + _openNewTab, + ) ?? true; + if (proceed) { + widget.editorController?.updateLink( + _displayTextFieldController.text.isEmpty + ? _linkTextFieldController.text + : _displayTextFieldController.text, + _linkTextFieldController.text, + _openNewTab, + widget.linkTagId + ); + } + Get.back(); + } + }, + ), + ], + ); + } + + @override + void dispose() { + _displayTextFieldFocusNode.dispose(); + _linkTextFieldFocusNode.dispose(); + _openNewTabFocusNode.dispose(); + _displayTextFieldController.dispose(); + _linkTextFieldController.dispose(); + super.dispose(); + } +} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 942919c5a5..5c03661a9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1058,8 +1058,8 @@ packages: dependency: "direct main" description: path: "." - ref: cnb_supported - resolved-ref: "978886d768e6540fc7dbe016dd83733c56ffb220" + ref: cherry-pick-insert-link-dialog + resolved-ref: "0286c90e75ae903bbb06ceab425f7cc8496d910b" url: "https://github.com/linagora/html-editor-enhanced.git" source: git version: "2.5.1" diff --git a/pubspec.yaml b/pubspec.yaml index dd8335bff2..fe26b690a3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,7 +60,7 @@ dependencies: html_editor_enhanced: git: url: https://github.com/linagora/html-editor-enhanced.git - ref: cnb_supported + ref: cherry-pick-insert-link-dialog # TODO: We will change it when the PR in upstream repository will be merged # https://github.com/linagora/jmap-dart-client/pull/87