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); + } +}