diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart index 40c52de2d..6a4c2d92c 100644 --- a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart @@ -2,20 +2,23 @@ import 'package:core/presentation/constants/constants_ui.dart'; import 'package:core/utils/html/html_template.dart'; import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/html/html_utils.dart'; import 'package:flutter/material.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart'; typedef OnCreatedEditorAction = Function(BuildContext context, HtmlEditorApi editorApi, String content); typedef OnLoadCompletedEditorAction = Function(HtmlEditorApi editorApi, WebUri? url); typedef OnEditorContentHeightChanged = Function(double height); +typedef OnTextSelectionChanged = Function(TextSelectionData?); -class MobileEditorWidget extends StatelessWidget { - +class MobileEditorWidget extends StatefulWidget { final String content; final TextDirection direction; final OnCreatedEditorAction onCreatedEditorAction; final OnLoadCompletedEditorAction onLoadCompletedEditorAction; final OnEditorContentHeightChanged? onEditorContentHeightChanged; + final OnTextSelectionChanged? onTextSelectionChanged; const MobileEditorWidget({ super.key, @@ -24,8 +27,47 @@ class MobileEditorWidget extends StatelessWidget { required this.onCreatedEditorAction, required this.onLoadCompletedEditorAction, this.onEditorContentHeightChanged, + this.onTextSelectionChanged, }); + @override + State createState() => _MobileEditorState(); +} + +class _MobileEditorState extends State with TextSelectionMixin { + + HtmlEditorApi? _editorApi; + + @override + void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged; + + void _setupSelectionListener() async { + final webViewController = _editorApi?.webViewController; + if (webViewController == null) return; + + webViewController.addJavaScriptHandler( + handlerName: HtmlUtils.registerSelectionChangeListener.name, + callback: (args) { + if (!mounted) return; + + if (args.isNotEmpty) { + final data = args[0] as Map; + handleSelectionChange(data); + } + }, + ); + + await webViewController.evaluateJavascript( + source: HtmlUtils.registerSelectionChangeListener.script, + ); + } + + @override + void dispose() { + _editorApi = null; + super.dispose(); + } + @override Widget build(BuildContext context) { return HtmlEditor( @@ -33,14 +75,18 @@ class MobileEditorWidget extends StatelessWidget { minHeight: 550, maxHeight: PlatformInfo.isIOS ? ConstantsUI.composerHtmlContentMaxHeight : null, addDefaultSelectionMenuItems: false, - initialContent: content, + initialContent: widget.content, customStyleCss: HtmlTemplate.mobileCustomInternalStyleCSS( - direction: direction, + direction: widget.direction, useDefaultFontStyle: true, ), - onCreated: (editorApi) => onCreatedEditorAction.call(context, editorApi, content), - onCompleted: onLoadCompletedEditorAction, - onContentHeightChanged: PlatformInfo.isIOS ? onEditorContentHeightChanged : null, + onCreated: (editorApi) { + _editorApi = editorApi; + widget.onCreatedEditorAction.call(context, editorApi, widget.content); + _setupSelectionListener(); + }, + onCompleted: widget.onLoadCompletedEditorAction, + onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null, ); } } 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 04041cf08..90f4e1dac 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -6,6 +6,7 @@ import 'package:core/utils/html/html_template.dart'; import 'package:core/utils/html/html_utils.dart'; import 'package:flutter/material.dart'; import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mixins/text_selection_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/widgets/web/signature_tooltip_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:universal_html/html.dart' hide VoidCallback; @@ -24,6 +25,7 @@ typedef OnPasteImageFailureAction = Function( UploadError uploadError); typedef OnInitialContentLoadComplete = Function(String text); typedef OnKeyDownEditorAction = Function(int? keyCode); +typedef OnTextSelectionChanged = Function(TextSelectionData?); class WebEditorWidget extends StatefulWidget { @@ -46,6 +48,7 @@ class WebEditorWidget extends StatefulWidget { final OnPasteImageFailureAction? onPasteImageFailureAction; final OnInitialContentLoadComplete? onInitialContentLoadComplete; final OnKeyDownEditorAction? onKeyDownEditorAction; + final OnTextSelectionChanged? onTextSelectionChanged; const WebEditorWidget({ super.key, @@ -68,29 +71,36 @@ class WebEditorWidget extends StatefulWidget { this.onPasteImageFailureAction, this.onInitialContentLoadComplete, this.onKeyDownEditorAction, + this.onTextSelectionChanged, }); @override State createState() => _WebEditorState(); } -class _WebEditorState extends State { +class _WebEditorState extends State with TextSelectionMixin { static const double _defaultHtmlEditorHeight = 550; late HtmlEditorController _editorController; bool _dropListenerRegistered = false; + bool _selectionChangeListenerRegistered = false; Function(Event)? _dropListener; + Function(Event)? _selectionChangeListener; OverlayEntry? _signatureTooltipEntry; final GlobalKey _signatureTooltipKey = GlobalKey(); double _signatureTooltipLeft = 0; bool _signatureTooltipReady = false; + @override + void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged; + @override void initState() { super.initState(); _editorController = widget.editorController; + _dropListener = (event) { if (event is MessageEvent) { if (jsonDecode(event.data)['name'] == HtmlUtils.registerDropListener.name) { @@ -101,6 +111,19 @@ class _WebEditorState extends State { if (_dropListener != null) { window.addEventListener("message", _dropListener!); } + + _selectionChangeListener = (event) { + if (event is MessageEvent) { + final data = jsonDecode(event.data); + + if (data['name'] == HtmlUtils.registerSelectionChangeListener.name) { + handleSelectionChange(data); + } + } + }; + if (_selectionChangeListener != null) { + window.addEventListener("message", _selectionChangeListener!); + } } @override @@ -120,6 +143,10 @@ class _WebEditorState extends State { window.removeEventListener("message", _dropListener!); _dropListener = null; } + if (_selectionChangeListener != null) { + window.removeEventListener("message", _selectionChangeListener!); + _selectionChangeListener = null; + } _hideSignatureTooltip(); super.dispose(); } @@ -163,6 +190,10 @@ class _WebEditorState extends State { name: HtmlUtils.unregisterDropListener.name, script: HtmlUtils.unregisterDropListener.script, ), + WebScript( + name: HtmlUtils.registerSelectionChangeListener.name, + script: HtmlUtils.registerSelectionChangeListener.script, + ), WebScript( name: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).name, script: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).script, @@ -184,6 +215,11 @@ class _WebEditorState extends State { HtmlUtils.registerDropListener.name); _dropListenerRegistered = true; } + if (!_selectionChangeListenerRegistered) { + _editorController.evaluateJavascriptWeb( + HtmlUtils.registerSelectionChangeListener.name); + _selectionChangeListenerRegistered = true; + } }, onFocus: widget.onFocus, onUnFocus: widget.onUnFocus,