diff --git a/core/lib/utils/html/html_utils.dart b/core/lib/utils/html/html_utils.dart
index 8c83d4503..656b2a994 100644
--- a/core/lib/utils/html/html_utils.dart
+++ b/core/lib/utils/html/html_utils.dart
@@ -48,6 +48,80 @@ class HtmlUtils {
editor.parentNode.replaceChild(newEditor, editor);''',
name: 'unregisterDropListener');
+ static const registerSelectionChangeListener = (
+ script: '''
+ let lastSelectedText = '';
+
+ document.addEventListener('selectionchange', function() {
+ const selection = window.getSelection();
+ const selectedText = selection ? selection.toString().trim() : '';
+
+ if (selectedText === lastSelectedText) {
+ return;
+ }
+ lastSelectedText = selectedText;
+
+ if (selectedText.length > 0 && selection.rangeCount > 0) {
+ const range = selection.getRangeAt(0);
+ const rects = range.getClientRects();
+
+ if (rects.length > 0) {
+ const rect = rects[rects.length - 1];
+
+ // When iframe
+ if (window.parent) {
+ window.parent.postMessage(
+ JSON.stringify({
+ name: 'onSelectionChange',
+ hasSelection: true,
+ selectedText: selectedText,
+ coordinates: {
+ x: rect.right,
+ y: rect.bottom,
+ width: rect.width,
+ height: rect.height
+ }
+ }),
+ "*"
+ )
+ }
+
+ // When WebView
+ if (window.flutter_inappwebview) {
+ window.flutter_inappwebview.callHandler('onSelectionChange', {
+ hasSelection: true,
+ selectedText: selectedText,
+ coordinates: {
+ x: rect.right,
+ y: rect.bottom,
+ width: rect.width,
+ height: rect.height
+ }
+ });
+ }
+ }
+ } else {
+ // When iframe
+ if (window.parent) {
+ window.parent.postMessage(
+ JSON.stringify({
+ name: 'onSelectionChange',
+ hasSelection: false
+ }),
+ "*"
+ )
+ }
+
+ // When WebView
+ if (window.flutter_inappwebview) {
+ window.flutter_inappwebview.callHandler('onSelectionChange', {
+ hasSelection: false
+ });
+ }
+ }
+ });''',
+ name: 'onSelectionChange');
+
static recalculateEditorHeight({double? maxHeight}) => (
script: '''
const editable = document.querySelector('.note-editable');
diff --git a/lib/features/composer/presentation/widgets/mixins/text_selection_mixin.dart b/lib/features/composer/presentation/widgets/mixins/text_selection_mixin.dart
new file mode 100644
index 000000000..da97d1d77
--- /dev/null
+++ b/lib/features/composer/presentation/widgets/mixins/text_selection_mixin.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+
+class TextSelectionData {
+ final bool hasSelection;
+ final String? selectedText;
+ final TextSelectionCoordinates? coordinates;
+
+ const TextSelectionData({
+ required this.hasSelection,
+ this.selectedText,
+ this.coordinates,
+ });
+
+ factory TextSelectionData.empty() {
+ return const TextSelectionData(hasSelection: false);
+ }
+
+ factory TextSelectionData.fromMap(Map data) {
+ try {
+ final hasSelection = data['hasSelection'] as bool? ?? false;
+
+ if (!hasSelection) {
+ return TextSelectionData.empty();
+ }
+
+ final selectedText = data['selectedText'] as String?;
+ final coordinatesData = data['coordinates'];
+
+ if (coordinatesData != null && selectedText != null) {
+ final coordinates = TextSelectionCoordinates.fromMap(coordinatesData);
+ return TextSelectionData(
+ hasSelection: true,
+ selectedText: selectedText,
+ coordinates: coordinates,
+ );
+ }
+
+ return TextSelectionData.empty();
+ } catch (e) {
+ return TextSelectionData.empty();
+ }
+ }
+}
+
+class TextSelectionCoordinates {
+ final double x;
+ final double y;
+ final double width;
+ final double height;
+
+ const TextSelectionCoordinates({
+ required this.x,
+ required this.y,
+ required this.width,
+ required this.height,
+ });
+
+ factory TextSelectionCoordinates.fromMap(Map data) {
+ return TextSelectionCoordinates(
+ x: (data['x'] as num?)?.toDouble() ?? 0.0,
+ y: (data['y'] as num?)?.toDouble() ?? 0.0,
+ width: (data['width'] as num?)?.toDouble() ?? 0.0,
+ height: (data['height'] as num?)?.toDouble() ?? 0.0,
+ );
+ }
+
+ Offset get position => Offset(x, y);
+}
+
+mixin TextSelectionMixin on State {
+ void Function(TextSelectionData?)? get onSelectionChanged => null;
+
+ void handleSelectionChange(Map data) {
+ final selectionData = TextSelectionData.fromMap(data);
+ onSelectionChanged?.call(selectionData);
+ }
+}