Implement text selection mixin in mobile and web editor
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user