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 a1a21c6ce7..a542d61a21 100644 --- a/core/lib/presentation/views/text/text_form_field_builder.dart +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; +typedef OnValidator = String? Function(String? value); + class TextFormFieldBuilder extends StatefulWidget { final ValueChanged? onTextChange; @@ -25,6 +27,7 @@ class TextFormFieldBuilder extends StatefulWidget { final bool readOnly; final MouseCursor? mouseCursor; final List? autofillHints; + final OnValidator? validator; const TextFormFieldBuilder({ super.key, @@ -49,6 +52,7 @@ class TextFormFieldBuilder extends StatefulWidget { this.onTap, this.onTextChange, this.onTextSubmitted, + this.validator, }); @override @@ -75,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, 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_widget_style.dart b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart new file mode 100644 index 0000000000..4ff7f09b41 --- /dev/null +++ b/lib/features/composer/presentation/styles/web/insert_link_dialog_widget_style.dart @@ -0,0 +1,90 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class InsertLinkDialogWidgetStyle { + static const double actionOverFlowButtonSpacing = 8.0; + static const double elevation = 10.0; + static const double widthRatio = 0.3; + static const double tittleToFieldSpace = 10.0; + static const double fieldToFieldSpace = 20.0; + static const double buttonRadius = 10.0; + static const double buttonHeight = 40.0; + + static const int maxLines = 1; + + static const TextStyle tittleStyle = TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.black + ); + static const TextStyle fieldTitleStyle = TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: Colors.black + ); + static const TextStyle textInputStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Colors.black + ); + static const TextStyle hintTextStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: AppColor.colorHintSearchBar + ); + static const TextStyle buttonCancelTextStyle = TextStyle( + color: AppColor.primaryColor, + fontSize: 17.0, + fontWeight: FontWeight.w500 + ); + static const TextStyle buttonInsertTextStyle = TextStyle( + color: Colors.white, + fontSize: 17.0, + fontWeight: FontWeight.w500 + ); + + static const EdgeInsetsGeometry tittlePadding = EdgeInsets.symmetric( + vertical: 16, + horizontal: 16 + ); + static const EdgeInsetsGeometry contentPadding = EdgeInsets.symmetric( + horizontal: 16 + ); + static const EdgeInsetsGeometry actionsPadding = EdgeInsets.symmetric( + vertical: 8, + horizontal: 16 + ); + static const EdgeInsetsGeometry textInputContentPadding = EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 10.0 + ); + + static const ShapeBorder shape = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)) + ); + static const InputBorder border = OutlineInputBorder( + borderSide: BorderSide( + color: AppColor.colorInputBorderCreateMailbox, + width: 1.0 + ), + borderRadius: BorderRadius.all(Radius.circular(10.0)) + ); + static const InputBorder focusedBorder = OutlineInputBorder( + borderSide: BorderSide( + color: AppColor.primaryColor, + width: 1.0 + ), + borderRadius: BorderRadius.all(Radius.circular(10.0)) + ); + + static const InputDecoration urlFieldDecoration = InputDecoration( + filled: true, + fillColor: Colors.white, + contentPadding: textInputContentPadding, + enabledBorder: border, + border: border, + focusedBorder: focusedBorder, + hintText: 'URL', + hintStyle: hintTextStyle, + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart index 5da136ef90..b40c7842c4 100644 --- a/lib/features/composer/presentation/view/web/web_editor_view.dart +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -30,6 +30,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { final double? width; final double? height; final VoidCallback? onDragEnter; + final OnEditLink? onEditLink; const WebEditorView({ super.key, @@ -47,6 +48,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { this.width, this.height, this.onDragEnter, + this.onEditLink, }); @override @@ -73,6 +75,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); case EmailActionType.editDraft: case EmailActionType.editSendingEmail: @@ -98,6 +101,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ), (success) { if (success is GetEmailContentLoading) { @@ -123,6 +127,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } @@ -155,6 +160,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); }, (success) { @@ -183,6 +189,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } @@ -202,6 +209,7 @@ class WebEditorView extends StatelessWidget with EditorViewMixin { width: width, height: height, onDragEnter: onDragEnter, + onEditLink: onEditLink, ); } } 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/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index a2ac28b523..97b97af1bc 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -12,6 +12,7 @@ typedef OnInitialContentEditorAction = Function(String text); typedef OnMouseDownEditorAction = Function(BuildContext context); typedef OnEditorSettingsChange = Function(EditorSettings settings); typedef OnEditorTextSizeChanged = Function(int? size); +typedef OnEditLink = Function(String? text, String? url, bool? isOpenNewTab, String linkTagId)?; class WebEditorWidget extends StatefulWidget { @@ -28,6 +29,7 @@ class WebEditorWidget extends StatefulWidget { final double? width; final double? height; final VoidCallback? onDragEnter; + final OnEditLink? onEditLink; const WebEditorWidget({ super.key, @@ -44,6 +46,7 @@ class WebEditorWidget extends StatefulWidget { this.width, this.height, this.onDragEnter, + this.onEditLink, }); @override @@ -176,6 +179,7 @@ class _WebEditorState extends State { ), onDragEnter: widget.onDragEnter, onDragLeave: () {}, + onEditLink: widget.onEditLink ), ); } diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index b563d04a00..f7fff0d73b 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -3873,5 +3873,29 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "insertLink": "Insert link", + "@insertLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textToDisplay": "Text to display", + "@textToDisplay": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toWhatURLShouldThisLinkGo": "To what URL should this link go?", + "@toWhatURLShouldThisLinkGo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pleaseEnterURL": "Please enter a URL", + "@pleaseEnterURL": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5dcf3481c2..6466e2258d 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -4043,4 +4043,32 @@ class AppLocalizations { 'Show less', name: 'showLess'); } + + String get insertLink { + return Intl.message( + 'Insert link', + name: 'insertLink', + ); + } + + String get textToDisplay { + return Intl.message( + 'Text to display', + name: 'textToDisplay', + ); + } + + String get toWhatURLShouldThisLinkGo { + return Intl.message( + 'To what URL should this link go?', + name: 'toWhatURLShouldThisLinkGo', + ); + } + + String get pleaseEnterURL { + return Intl.message( + 'Please enter a URL', + name: 'pleaseEnterURL', + ); + } } \ 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