Add text selection mixin

This mixin leverages HTML callbacks that can be set by WebView editor
to send text selection event to the composer so the composer will
receive text selection event necessary to display buttons related to
text selection.
This commit is contained in:
Théo Poizat
2025-12-04 17:00:08 +01:00
committed by Dat H. Pham
parent 2ccb203610
commit 4013973110
2 changed files with 151 additions and 0 deletions
+74
View File
@@ -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');
@@ -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<dynamic, dynamic> 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<dynamic, dynamic> 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<T extends StatefulWidget> on State<T> {
void Function(TextSelectionData?)? get onSelectionChanged => null;
void handleSelectionChange(Map<dynamic, dynamic> data) {
final selectionData = TextSelectionData.fromMap(data);
onSelectionChanged?.call(selectionData);
}
}