Hide keyboard and selection icons when Scribe mobile modal opened
When opening Scribe mobile modal, we now unfocus the composer to hide the keyboard and the selection start and end icons from the OS. Add new methods to save, restore, and clear text selection in HTML editor. These methods allow preserving and restoring user selection state when opening the Scribe mobile modal so that the "replace" action still works.
This commit is contained in:
@@ -214,6 +214,40 @@ class HtmlUtils {
|
|||||||
})();''',
|
})();''',
|
||||||
name: 'deleteSelectionContent');
|
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}) => (
|
static recalculateEditorHeight({double? maxHeight}) => (
|
||||||
script: '''
|
script: '''
|
||||||
const editable = document.querySelector('.note-editable');
|
const editable = document.querySelector('.note-editable');
|
||||||
|
|||||||
+90
@@ -71,6 +71,83 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> 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<bool> 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<void> 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<void> unfocusEditor() async {
|
||||||
|
final editorApi = richTextMobileTabletController?.htmlEditorApi;
|
||||||
|
if (PlatformInfo.isIOS) {
|
||||||
|
await editorApi?.unfocus();
|
||||||
|
} else if (PlatformInfo.isAndroid) {
|
||||||
|
await editorApi?.hideKeyboard();
|
||||||
|
await editorApi?.unfocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveAndUnfocusForModal() async {
|
||||||
|
final saved = await saveSelection();
|
||||||
|
if (saved) {
|
||||||
|
await unfocusEditor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void clearTextInEditor() {
|
void clearTextInEditor() {
|
||||||
try {
|
try {
|
||||||
if (PlatformInfo.isWeb) {
|
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
|
// If there is a selection, it will replace the selection, else it will replace everything
|
||||||
Future<void> onReplaceTextCallback(String text) async {
|
Future<void> onReplaceTextCallback(String text) async {
|
||||||
final selection = editorTextSelection.value?.selectedText;
|
final selection = editorTextSelection.value?.selectedText;
|
||||||
|
|
||||||
|
if (PlatformInfo.isMobile) {
|
||||||
|
await restoreSelection();
|
||||||
|
}
|
||||||
|
|
||||||
if (selection == null || selection.isEmpty) {
|
if (selection == null || selection.isEmpty) {
|
||||||
clearTextInEditor();
|
clearTextInEditor();
|
||||||
}
|
}
|
||||||
@@ -103,6 +185,10 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
clearFocusRecipients();
|
clearFocusRecipients();
|
||||||
clearFocusSubject();
|
clearFocusSubject();
|
||||||
|
|
||||||
|
if (PlatformInfo.isMobile) {
|
||||||
|
await saveAndUnfocusForModal();
|
||||||
|
}
|
||||||
|
|
||||||
final fullText = await _getTextOnlyContentInEditor();
|
final fullText = await _getTextOnlyContentInEditor();
|
||||||
|
|
||||||
await AiScribeModalManager.showAIScribeMenuModal(
|
await AiScribeModalManager.showAIScribeMenuModal(
|
||||||
@@ -129,6 +215,10 @@ extension HandleAiScribeInComposerExtension on ComposerController {
|
|||||||
await onInsertTextCallback(suggestionText);
|
await onInsertTextCallback(suggestionText);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PlatformInfo.isMobile) {
|
||||||
|
await clearSavedSelection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleTextSelection(TextSelectionData? textSelectionData) {
|
void handleTextSelection(TextSelectionData? textSelectionData) {
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.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: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/composer_controller.dart';
|
||||||
import 'package:tmail_ui_user/features/composer/presentation/extensions/ai_scribe/handle_ai_scribe_in_composer_extension.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<void> _clearComposerInputFocus() async {
|
||||||
controller.clearFocusRecipients();
|
controller.clearFocusRecipients();
|
||||||
controller.clearFocusSubject();
|
controller.clearFocusSubject();
|
||||||
|
|
||||||
|
if (PlatformInfo.isMobile) {
|
||||||
|
await controller.saveAndUnfocusForModal();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:core/presentation/resources/image_paths.dart';
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
import 'package:core/presentation/views/button/tmail_button_widget.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:scribe/scribe.dart';
|
import 'package:scribe/scribe.dart';
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ class InlineAiAssistButton extends StatelessWidget {
|
|||||||
final ImagePaths imagePaths;
|
final ImagePaths imagePaths;
|
||||||
final String? selectedText;
|
final String? selectedText;
|
||||||
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
|
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
|
||||||
final VoidCallback? onTapFallback;
|
final AsyncCallback? onTapFallback;
|
||||||
|
|
||||||
const InlineAiAssistButton({
|
const InlineAiAssistButton({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -42,7 +43,7 @@ class InlineAiAssistButton extends StatelessWidget {
|
|||||||
size = renderBox.size;
|
size = renderBox.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
onTapFallback?.call();
|
await onTapFallback?.call();
|
||||||
|
|
||||||
await AiScribeModalManager.showAIScribeMenuModal(
|
await AiScribeModalManager.showAIScribeMenuModal(
|
||||||
imagePaths: imagePaths,
|
imagePaths: imagePaths,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:core/presentation/resources/image_paths.dart';
|
import 'package:core/presentation/resources/image_paths.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
import 'package:pointer_interceptor/pointer_interceptor.dart';
|
||||||
import 'package:scribe/scribe.dart';
|
import 'package:scribe/scribe.dart';
|
||||||
@@ -15,7 +16,7 @@ class AiSelectionOverlay extends StatelessWidget {
|
|||||||
final TextSelectionModel? selection;
|
final TextSelectionModel? selection;
|
||||||
final ImagePaths imagePaths;
|
final ImagePaths imagePaths;
|
||||||
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
|
final OnSelectAiScribeSuggestionAction onSelectAiScribeSuggestionAction;
|
||||||
final VoidCallback? onTapFallback;
|
final AsyncCallback? onTapFallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
Reference in New Issue
Block a user