Implement text selection mixin in mobile and web editor

This commit is contained in:
Théo Poizat
2025-12-04 17:00:50 +01:00
committed by Dat H. Pham
parent aaf60cf222
commit 57124f70a8
2 changed files with 90 additions and 8 deletions
@@ -2,20 +2,23 @@
import 'package:core/presentation/constants/constants_ui.dart'; import 'package:core/presentation/constants/constants_ui.dart';
import 'package:core/utils/html/html_template.dart'; import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/platform_info.dart'; import 'package:core/utils/platform_info.dart';
import 'package:core/utils/html/html_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:rich_text_composer/rich_text_composer.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 OnCreatedEditorAction = Function(BuildContext context, HtmlEditorApi editorApi, String content);
typedef OnLoadCompletedEditorAction = Function(HtmlEditorApi editorApi, WebUri? url); typedef OnLoadCompletedEditorAction = Function(HtmlEditorApi editorApi, WebUri? url);
typedef OnEditorContentHeightChanged = Function(double height); typedef OnEditorContentHeightChanged = Function(double height);
typedef OnTextSelectionChanged = Function(TextSelectionData?);
class MobileEditorWidget extends StatelessWidget { class MobileEditorWidget extends StatefulWidget {
final String content; final String content;
final TextDirection direction; final TextDirection direction;
final OnCreatedEditorAction onCreatedEditorAction; final OnCreatedEditorAction onCreatedEditorAction;
final OnLoadCompletedEditorAction onLoadCompletedEditorAction; final OnLoadCompletedEditorAction onLoadCompletedEditorAction;
final OnEditorContentHeightChanged? onEditorContentHeightChanged; final OnEditorContentHeightChanged? onEditorContentHeightChanged;
final OnTextSelectionChanged? onTextSelectionChanged;
const MobileEditorWidget({ const MobileEditorWidget({
super.key, super.key,
@@ -24,8 +27,47 @@ class MobileEditorWidget extends StatelessWidget {
required this.onCreatedEditorAction, required this.onCreatedEditorAction,
required this.onLoadCompletedEditorAction, required this.onLoadCompletedEditorAction,
this.onEditorContentHeightChanged, this.onEditorContentHeightChanged,
this.onTextSelectionChanged,
}); });
@override
State<MobileEditorWidget> createState() => _MobileEditorState();
}
class _MobileEditorState extends State<MobileEditorWidget> 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<dynamic, dynamic>;
handleSelectionChange(data);
}
},
);
await webViewController.evaluateJavascript(
source: HtmlUtils.registerSelectionChangeListener.script,
);
}
@override
void dispose() {
_editorApi = null;
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return HtmlEditor( return HtmlEditor(
@@ -33,14 +75,18 @@ class MobileEditorWidget extends StatelessWidget {
minHeight: 550, minHeight: 550,
maxHeight: PlatformInfo.isIOS ? ConstantsUI.composerHtmlContentMaxHeight : null, maxHeight: PlatformInfo.isIOS ? ConstantsUI.composerHtmlContentMaxHeight : null,
addDefaultSelectionMenuItems: false, addDefaultSelectionMenuItems: false,
initialContent: content, initialContent: widget.content,
customStyleCss: HtmlTemplate.mobileCustomInternalStyleCSS( customStyleCss: HtmlTemplate.mobileCustomInternalStyleCSS(
direction: direction, direction: widget.direction,
useDefaultFontStyle: true, useDefaultFontStyle: true,
), ),
onCreated: (editorApi) => onCreatedEditorAction.call(context, editorApi, content), onCreated: (editorApi) {
onCompleted: onLoadCompletedEditorAction, _editorApi = editorApi;
onContentHeightChanged: PlatformInfo.isIOS ? onEditorContentHeightChanged : null, widget.onCreatedEditorAction.call(context, editorApi, widget.content);
_setupSelectionListener();
},
onCompleted: widget.onLoadCompletedEditorAction,
onContentHeightChanged: PlatformInfo.isIOS ? widget.onEditorContentHeightChanged : null,
); );
} }
} }
@@ -6,6 +6,7 @@ import 'package:core/utils/html/html_template.dart';
import 'package:core/utils/html/html_utils.dart'; import 'package:core/utils/html/html_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:html_editor_enhanced/html_editor.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/features/composer/presentation/widgets/web/signature_tooltip_widget.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:universal_html/html.dart' hide VoidCallback; import 'package:universal_html/html.dart' hide VoidCallback;
@@ -24,6 +25,7 @@ typedef OnPasteImageFailureAction = Function(
UploadError uploadError); UploadError uploadError);
typedef OnInitialContentLoadComplete = Function(String text); typedef OnInitialContentLoadComplete = Function(String text);
typedef OnKeyDownEditorAction = Function(int? keyCode); typedef OnKeyDownEditorAction = Function(int? keyCode);
typedef OnTextSelectionChanged = Function(TextSelectionData?);
class WebEditorWidget extends StatefulWidget { class WebEditorWidget extends StatefulWidget {
@@ -46,6 +48,7 @@ class WebEditorWidget extends StatefulWidget {
final OnPasteImageFailureAction? onPasteImageFailureAction; final OnPasteImageFailureAction? onPasteImageFailureAction;
final OnInitialContentLoadComplete? onInitialContentLoadComplete; final OnInitialContentLoadComplete? onInitialContentLoadComplete;
final OnKeyDownEditorAction? onKeyDownEditorAction; final OnKeyDownEditorAction? onKeyDownEditorAction;
final OnTextSelectionChanged? onTextSelectionChanged;
const WebEditorWidget({ const WebEditorWidget({
super.key, super.key,
@@ -68,29 +71,36 @@ class WebEditorWidget extends StatefulWidget {
this.onPasteImageFailureAction, this.onPasteImageFailureAction,
this.onInitialContentLoadComplete, this.onInitialContentLoadComplete,
this.onKeyDownEditorAction, this.onKeyDownEditorAction,
this.onTextSelectionChanged,
}); });
@override @override
State<WebEditorWidget> createState() => _WebEditorState(); State<WebEditorWidget> createState() => _WebEditorState();
} }
class _WebEditorState extends State<WebEditorWidget> { class _WebEditorState extends State<WebEditorWidget> with TextSelectionMixin {
static const double _defaultHtmlEditorHeight = 550; static const double _defaultHtmlEditorHeight = 550;
late HtmlEditorController _editorController; late HtmlEditorController _editorController;
bool _dropListenerRegistered = false; bool _dropListenerRegistered = false;
bool _selectionChangeListenerRegistered = false;
Function(Event)? _dropListener; Function(Event)? _dropListener;
Function(Event)? _selectionChangeListener;
OverlayEntry? _signatureTooltipEntry; OverlayEntry? _signatureTooltipEntry;
final GlobalKey _signatureTooltipKey = GlobalKey(); final GlobalKey _signatureTooltipKey = GlobalKey();
double _signatureTooltipLeft = 0; double _signatureTooltipLeft = 0;
bool _signatureTooltipReady = false; bool _signatureTooltipReady = false;
@override
void Function(TextSelectionData?)? get onSelectionChanged => widget.onTextSelectionChanged;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_editorController = widget.editorController; _editorController = widget.editorController;
_dropListener = (event) { _dropListener = (event) {
if (event is MessageEvent) { if (event is MessageEvent) {
if (jsonDecode(event.data)['name'] == HtmlUtils.registerDropListener.name) { if (jsonDecode(event.data)['name'] == HtmlUtils.registerDropListener.name) {
@@ -101,6 +111,19 @@ class _WebEditorState extends State<WebEditorWidget> {
if (_dropListener != null) { if (_dropListener != null) {
window.addEventListener("message", _dropListener!); 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 @override
@@ -120,6 +143,10 @@ class _WebEditorState extends State<WebEditorWidget> {
window.removeEventListener("message", _dropListener!); window.removeEventListener("message", _dropListener!);
_dropListener = null; _dropListener = null;
} }
if (_selectionChangeListener != null) {
window.removeEventListener("message", _selectionChangeListener!);
_selectionChangeListener = null;
}
_hideSignatureTooltip(); _hideSignatureTooltip();
super.dispose(); super.dispose();
} }
@@ -163,6 +190,10 @@ class _WebEditorState extends State<WebEditorWidget> {
name: HtmlUtils.unregisterDropListener.name, name: HtmlUtils.unregisterDropListener.name,
script: HtmlUtils.unregisterDropListener.script, script: HtmlUtils.unregisterDropListener.script,
), ),
WebScript(
name: HtmlUtils.registerSelectionChangeListener.name,
script: HtmlUtils.registerSelectionChangeListener.script,
),
WebScript( WebScript(
name: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).name, name: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).name,
script: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).script, script: HtmlUtils.recalculateEditorHeight(maxHeight: maxHeight).script,
@@ -184,6 +215,11 @@ class _WebEditorState extends State<WebEditorWidget> {
HtmlUtils.registerDropListener.name); HtmlUtils.registerDropListener.name);
_dropListenerRegistered = true; _dropListenerRegistered = true;
} }
if (!_selectionChangeListenerRegistered) {
_editorController.evaluateJavascriptWeb(
HtmlUtils.registerSelectionChangeListener.name);
_selectionChangeListenerRegistered = true;
}
}, },
onFocus: widget.onFocus, onFocus: widget.onFocus,
onUnFocus: widget.onUnFocus, onUnFocus: widget.onUnFocus,