diff --git a/lib/src/models/config/quill_configurations.dart b/lib/src/models/config/quill_configurations.dart index 853136b7..a07ecf1e 100644 --- a/lib/src/models/config/quill_configurations.dart +++ b/lib/src/models/config/quill_configurations.dart @@ -1,3 +1,4 @@ export 'editor/editor_configurations.dart'; +export 'quill_controller_configurations.dart'; export 'quill_shared_configurations.dart'; export 'toolbar/simple_toolbar_configurations.dart'; diff --git a/lib/src/models/config/quill_controller_configurations.dart b/lib/src/models/config/quill_controller_configurations.dart new file mode 100644 index 00000000..ae38841a --- /dev/null +++ b/lib/src/models/config/quill_controller_configurations.dart @@ -0,0 +1,8 @@ +class QuillControllerConfigurations { + const QuillControllerConfigurations({this.onClipboardPaste}); + + /// Callback when the user pastes and data has not already been processed + /// + /// Return true if the paste operation was handled + final Future Function()? onClipboardPaste; +} diff --git a/lib/src/widgets/quill/quill_controller.dart b/lib/src/widgets/quill/quill_controller.dart index fcb6db69..c8e6a7c3 100644 --- a/lib/src/widgets/quill/quill_controller.dart +++ b/lib/src/widgets/quill/quill_controller.dart @@ -18,6 +18,7 @@ class QuillController extends ChangeNotifier { QuillController({ required Document document, required TextSelection selection, + this.configurations = const QuillControllerConfigurations(), this.keepStyleOnNewLine = true, this.onReplaceText, this.onDelete, @@ -27,13 +28,18 @@ class QuillController extends ChangeNotifier { }) : _document = document, _selection = selection; - factory QuillController.basic() { + factory QuillController.basic( + {QuillControllerConfigurations configurations = + const QuillControllerConfigurations()}) { return QuillController( + configurations: configurations, document: Document(), selection: const TextSelection.collapsed(offset: 0), ); } + final QuillControllerConfigurations configurations; + /// Document managed by this controller. Document _document; @@ -505,34 +511,10 @@ class QuillController extends ChangeNotifier { return false; } - /// Returns whether paste was handled here. + /// Returns whether paste operation was handled here. + /// updateEditor is called if paste operation was successful. Future clipboardPaste({void Function()? updateEditor}) async { - if (readOnly) return true; - - // When image copied internally in the editor - if (_copiedImageUrl != null) { - final index = selection.baseOffset; - final length = selection.extentOffset - index; - replaceText( - index, - length, - BlockEmbed.image(_copiedImageUrl!.url), - null, - ); - if (_copiedImageUrl!.styleString.isNotEmpty) { - formatText( - getEmbedNode(this, index + 1).offset, - 1, - StyleAttribute(_copiedImageUrl!.styleString), - ); - } - copiedImageUrl = null; - return true; - } - - if (!selection.isValid) { - return true; - } + if (readOnly || !selection.isValid) return true; if (await _pasteHTML()) { updateEditor?.call(); @@ -553,6 +535,12 @@ class QuillController extends ChangeNotifier { updateEditor?.call(); return true; } + + if (await configurations.onClipboardPaste?.call() == true) { + updateEditor?.call(); + return true; + } + return false; } diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 94e9b4d0..e452f4fd 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -11,9 +11,10 @@ import 'package:flutter/scheduler.dart' show SchedulerBinding; import 'package:flutter/services.dart' show Clipboard, + ClipboardData, HardwareKeyboard, - LogicalKeyboardKey, KeyDownEvent, + LogicalKeyboardKey, SystemChannels, TextInputControl; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' @@ -31,6 +32,7 @@ import '../../models/structs/offset_value.dart'; import '../../models/structs/vertical_spacing.dart'; import '../../utils/cast.dart'; import '../../utils/delta.dart'; +import '../../utils/embeds.dart'; import '../../utils/platform.dart'; import '../editor/editor.dart'; import '../others/cursor.dart'; @@ -148,8 +150,37 @@ class QuillRawEditorState extends EditorState /// Paste text from [Clipboard]. @override Future pasteText(SelectionChangedCause cause) async { - if (await controller.clipboardPaste( - updateEditor: () => bringIntoView(textEditingValue.selection.extent))) { + if (controller.readOnly) { + return; + } + + // When image copied internally in the editor + final copiedImageUrl = controller.copiedImageUrl; + if (copiedImageUrl != null) { + final index = textEditingValue.selection.baseOffset; + final length = textEditingValue.selection.extentOffset - index; + controller.replaceText( + index, + length, + BlockEmbed.image(copiedImageUrl.url), + null, + ); + if (copiedImageUrl.styleString.isNotEmpty) { + controller.formatText( + getEmbedNode(controller, index + 1).offset, + 1, + StyleAttribute(copiedImageUrl.styleString), + ); + } + controller.copiedImageUrl = null; + await Clipboard.setData( + const ClipboardData(text: ''), + ); + return; + } + + if (await controller.clipboardPaste()) { + bringIntoView(textEditingValue.selection.extent); return; } diff --git a/lib/src/widgets/toolbar/buttons/clipboard_button.dart b/lib/src/widgets/toolbar/buttons/clipboard_button.dart index e86e1c75..93d1ba35 100644 --- a/lib/src/widgets/toolbar/buttons/clipboard_button.dart +++ b/lib/src/widgets/toolbar/buttons/clipboard_button.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:super_clipboard/super_clipboard.dart'; import '../../../../extensions.dart'; import '../../../../flutter_quill.dart'; @@ -11,19 +12,28 @@ import '../base_button/base_value_button.dart'; enum ClipboardAction { cut, copy, paste } class ClipboardMonitor { - final ClipboardStatusNotifier _clipboardStatus = ClipboardStatusNotifier(); - + bool _canPaste = false; + bool get canPaste => _canPaste; Timer? _timer; void monitorClipboard(bool add, void Function() listener) { if (kIsWeb) return; if (add) { - _clipboardStatus.addListener(listener); _timer = Timer.periodic( - const Duration(seconds: 1), (timer) => _clipboardStatus.update()); + const Duration(seconds: 1), (timer) => _update(listener)); } else { _timer?.cancel(); - _clipboardStatus.removeListener(listener); + } + } + + Future _update(void Function() listener) async { + final reader = await SystemClipboard.instance?.read(); + if (reader != null) { + final available = reader.platformFormats; + if (_canPaste != available.isNotEmpty) { + _canPaste = available.isNotEmpty; + listener(); + } } } } @@ -54,10 +64,7 @@ class QuillToolbarClipboardButtonState case ClipboardAction.copy: return !controller.selection.isCollapsed; case ClipboardAction.paste: - return !controller.readOnly && - (kIsWeb || - widget._monitor._clipboardStatus.value == - ClipboardStatus.pasteable); + return !controller.readOnly && (kIsWeb || widget._monitor.canPaste); } }