diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart index 5f2985149..45d9d4619 100644 --- a/core/lib/utils/html/html_utils.dart +++ b/core/lib/utils/html/html_utils.dart @@ -214,6 +214,40 @@ class HtmlUtils { })();''', name: 'deleteSelectionContent'); + static const saveSelection = ( + script: ''' + (() => { + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + window._savedRange = selection.getRangeAt(0).cloneRange(); + return true; + } + return false; + })();''', + name: 'saveSelection'); + + static const restoreSelection = ( + script: ''' + (() => { + if (window._savedRange) { + const selection = window.getSelection(); + if (selection) { + selection.removeAllRanges(); + selection.addRange(window._savedRange); + return true; + } + } + return false; + })();''', + name: 'restoreSelection'); + + static const clearSavedSelection = ( + script: ''' + (() => { + delete window._savedRange; + })();''', + name: 'clearSavedSelection'); + static recalculateEditorHeight({double? maxHeight}) => ( script: ''' const editable = document.querySelector('.note-editable'); diff --git a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart index 0304f1ccb..7b3e5c094 100644 --- a/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart +++ b/lib/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart @@ -71,6 +71,83 @@ extension HandleAiScribeInComposerExtension on ComposerController { } } + Future saveSelection() async { + try { + if (PlatformInfo.isWeb) { + final result = await richTextWebController?.editorController.evaluateJavascriptWeb( + HtmlUtils.saveSelection.name, + hasReturnValue: true, + ) ?? false; + return result; + } else { + final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController + .evaluateJavascript( + source: HtmlUtils.saveSelection.script, + ) ?? false; + return result; + } + } catch (e) { + logError('$runtimeType::saveSelection:Exception = $e'); + return false; + } + } + + Future restoreSelection() async { + try { + if (PlatformInfo.isWeb) { + final result = await richTextWebController?.editorController.evaluateJavascriptWeb( + HtmlUtils.restoreSelection.name, + hasReturnValue: true, + ) ?? false; + return result; + } else { + final result = await richTextMobileTabletController?.htmlEditorApi?.webViewController + .evaluateJavascript( + source: HtmlUtils.restoreSelection.script, + ) ?? false; + return result; + } + } catch (e) { + logError('$runtimeType::restoreSelection:Exception = $e'); + return false; + } + } + + Future clearSavedSelection() async { + try { + if (PlatformInfo.isWeb) { + await richTextWebController?.editorController.evaluateJavascriptWeb( + HtmlUtils.clearSavedSelection.name, + hasReturnValue: false, + ); + } else { + await richTextMobileTabletController?.htmlEditorApi?.webViewController + .evaluateJavascript( + source: HtmlUtils.clearSavedSelection.script, + ); + } + } catch (e) { + logError('$runtimeType::clearSavedSelection:Exception = $e'); + } + } + + Future unfocusEditor() async { + final editorApi = richTextMobileTabletController?.htmlEditorApi; + if (PlatformInfo.isIOS) { + await editorApi?.unfocus(); + } else if (PlatformInfo.isAndroid) { + await editorApi?.hideKeyboard(); + await editorApi?.unfocus(); + } + } + + Future saveAndUnfocusForModal() async { + final saved = await saveSelection(); + if (saved) { + await unfocusEditor(); + } + } + void clearTextInEditor() { try { if (PlatformInfo.isWeb) { @@ -92,6 +169,11 @@ extension HandleAiScribeInComposerExtension on ComposerController { // If there is a selection, it will replace the selection, else it will replace everything Future onReplaceTextCallback(String text) async { final selection = editorTextSelection.value?.selectedText; + + if (PlatformInfo.isMobile) { + await restoreSelection(); + } + if (selection == null || selection.isEmpty) { clearTextInEditor(); } @@ -103,6 +185,10 @@ extension HandleAiScribeInComposerExtension on ComposerController { clearFocusRecipients(); clearFocusSubject(); + if (PlatformInfo.isMobile) { + await saveAndUnfocusForModal(); + } + final fullText = await _getTextOnlyContentInEditor(); await AiScribeModalManager.showAIScribeMenuModal( @@ -129,6 +215,10 @@ extension HandleAiScribeInComposerExtension on ComposerController { await onInsertTextCallback(suggestionText); break; } + + if (PlatformInfo.isMobile) { + await clearSavedSelection(); + } } void handleTextSelection(TextSelectionData? textSelectionData) { diff --git a/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart b/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart index eb859653b..46b072066 100644 --- a/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart +++ b/lib/features/composer/presentation/widgets/ai_scribe/composer_ai_scribe_selection_overlay.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:scribe/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.dart'; @@ -29,8 +30,12 @@ class ComposerAiScribeSelectionOverlay extends StatelessWidget { }); } - void _clearComposerInputFocus() { + Future _clearComposerInputFocus() async { controller.clearFocusRecipients(); controller.clearFocusSubject(); + + if (PlatformInfo.isMobile) { + await controller.saveAndUnfocusForModal(); + } } } diff --git a/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart b/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart index 06d3524ad..2ec3fa582 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/button/inline_ai_assist_button.dart @@ -1,5 +1,6 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:scribe/scribe.dart'; @@ -7,7 +8,7 @@ class InlineAiAssistButton extends StatelessWidget { final ImagePaths imagePaths; final String? selectedText; final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction; - final VoidCallback? onTapFallback; + final AsyncCallback? onTapFallback; const InlineAiAssistButton({ super.key, @@ -42,7 +43,7 @@ class InlineAiAssistButton extends StatelessWidget { size = renderBox.size; } - onTapFallback?.call(); + await onTapFallback?.call(); await AiScribeModalManager.showAIScribeMenuModal( imagePaths: imagePaths, diff --git a/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart b/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart index ea0621c4d..ba148bee5 100644 --- a/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart +++ b/scribe/lib/scribe/ai/presentation/widgets/overlay/ai_selection_overlay.dart @@ -1,4 +1,5 @@ import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:scribe/scribe.dart'; @@ -15,7 +16,7 @@ class AiSelectionOverlay extends StatelessWidget { final TextSelectionModel? selection; final ImagePaths imagePaths; final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction; - final VoidCallback? onTapFallback; + final AsyncCallback? onTapFallback; @override Widget build(BuildContext context) {