! Support clipboard actions from the toolbar. (#1843)

pull/1855/head
AtlasAutocode 11 months ago committed by GitHub
parent b7711bb46d
commit 1a4109fb7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      example/lib/screens/quill/quill_screen.dart
  2. 12
      lib/src/l10n/generated/quill_localizations.dart
  3. 2
      lib/src/l10n/generated/quill_localizations_da.dart
  4. 10
      lib/src/l10n/generated/quill_localizations_en.dart
  5. 2
      lib/src/l10n/generated/quill_localizations_ko.dart
  6. 2
      lib/src/l10n/generated/quill_localizations_ms.dart
  7. 2
      lib/src/l10n/generated/quill_localizations_nl.dart
  8. 2
      lib/src/l10n/generated/quill_localizations_pl.dart
  9. 2
      lib/src/l10n/generated/quill_localizations_pt.dart
  10. 2
      lib/src/l10n/generated/quill_localizations_tk.dart
  11. 14
      lib/src/models/config/editor/editor_configurations.dart
  12. 1
      lib/src/models/config/quill_configurations.dart
  13. 8
      lib/src/models/config/quill_controller_configurations.dart
  14. 7
      lib/src/models/config/toolbar/simple_toolbar_button_options.dart
  15. 6
      lib/src/models/config/toolbar/simple_toolbar_configurations.dart
  16. 14
      lib/src/models/documents/nodes/line.dart
  17. 170
      lib/src/widgets/quill/quill_controller.dart
  18. 106
      lib/src/widgets/raw_editor/raw_editor_state.dart
  19. 61
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  20. 21
      lib/src/widgets/toolbar/base_button/base_value_button.dart
  21. 1
      lib/src/widgets/toolbar/base_toolbar.dart
  22. 144
      lib/src/widgets/toolbar/buttons/clipboard_button.dart
  23. 11
      lib/src/widgets/toolbar/buttons/toggle_style_button.dart
  24. 72
      lib/src/widgets/toolbar/simple_toolbar.dart
  25. 1
      test/bug_fix_test.dart
  26. 53
      test/widgets/controller_test.dart
  27. 3
      test/widgets/editor_test.dart

@ -59,6 +59,7 @@ class _QuillScreenState extends State<QuillScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
_controller.readOnly = _isReadOnly;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Flutter Quill'), title: const Text('Flutter Quill'),
@ -152,7 +153,6 @@ class _QuillScreenState extends State<QuillScreen> {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
sharedConfigurations: _sharedConfigurations, sharedConfigurations: _sharedConfigurations,
controller: _controller, controller: _controller,
readOnly: _isReadOnly,
), ),
scrollController: _editorScrollController, scrollController: _editorScrollController,
focusNode: _editorFocusNode, focusNode: _editorFocusNode,

@ -211,6 +211,18 @@ abstract class FlutterQuillLocalizations {
/// **'Copy'** /// **'Copy'**
String get copy; String get copy;
/// No description provided for @cut.
///
/// In en, this message translates to:
/// **'Cut'**
String get cut => 'Cut';
/// No description provided for @paste.
///
/// In en, this message translates to:
/// **'Paste'**
String get paste => 'Paste';
/// No description provided for @remove. /// No description provided for @remove.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsDa extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -25,6 +25,12 @@ class FlutterQuillLocalizationsEn extends FlutterQuillLocalizations {
@override @override
String get copy => 'Copy'; String get copy => 'Copy';
@override
String get cut => 'Cut';
@override
String get paste => 'Paste';
@override @override
String get remove => 'Remove'; String get remove => 'Remove';
@ -128,7 +134,7 @@ class FlutterQuillLocalizationsEn extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';
@ -402,7 +408,7 @@ class FlutterQuillLocalizationsEnUs extends FlutterQuillLocalizationsEn {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsKo extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsMs extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsNl extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsPl extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsPt extends FlutterQuillLocalizations {
String get alignRight => 'Align right'; String get alignRight => 'Align right';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Text direction'; String get textDirection => 'Text direction';

@ -128,7 +128,7 @@ class FlutterQuillLocalizationsTk extends FlutterQuillLocalizations {
String get alignRight => 'Saga deňleşdir'; String get alignRight => 'Saga deňleşdir';
@override @override
String get justifyWinWidth => 'Justify win width'; String get justifyWinWidth => 'Justify';
@override @override
String get textDirection => 'Tekst ugry'; String get textDirection => 'Tekst ugry';

@ -32,7 +32,6 @@ class QuillEditorConfigurations extends Equatable {
this.autoFocus = false, this.autoFocus = false,
this.expands = false, this.expands = false,
this.placeholder, this.placeholder,
this.readOnly = false,
this.checkBoxReadOnly, this.checkBoxReadOnly,
this.disableClipboard = false, this.disableClipboard = false,
this.textSelectionThemeData, this.textSelectionThemeData,
@ -96,7 +95,7 @@ class QuillEditorConfigurations extends Equatable {
/// by any shortcut or keyboard operation. The text is still selectable. /// by any shortcut or keyboard operation. The text is still selectable.
/// ///
/// Defaults to `false`. Must not be `null`. /// Defaults to `false`. Must not be `null`.
final bool readOnly; bool get readOnly => controller.readOnly;
/// Override [readOnly] for checkbox. /// Override [readOnly] for checkbox.
/// ///
@ -142,11 +141,11 @@ class QuillEditorConfigurations extends Equatable {
/// Whether the [onTapOutside] should be triggered or not /// Whether the [onTapOutside] should be triggered or not
/// Defaults to `true` /// Defaults to `true`
/// it have default implementation, check [onTapOuside] for more /// it have default implementation, check [onTapOutside] for more
final bool isOnTapOutsideEnabled; final bool isOnTapOutsideEnabled;
/// This will run only when [isOnTapOutsideEnabled] is true /// This will run only when [isOnTapOutsideEnabled] is true
/// by default on desktop and web it will unfocus /// by default on desktop and web it will un-focus
/// on mobile it will only unFocus if the kind property of /// on mobile it will only unFocus if the kind property of
/// event [PointerDownEvent] is [PointerDeviceKind.unknown] /// event [PointerDownEvent] is [PointerDeviceKind.unknown]
/// you can override this to fit your needs /// you can override this to fit your needs
@ -313,7 +312,7 @@ class QuillEditorConfigurations extends Equatable {
/// Additional list if links prefixes, which must not be prepended /// Additional list if links prefixes, which must not be prepended
/// with "https://" when [LinkMenuAction.launch] happened /// with "https://" when [LinkMenuAction.launch] happened
/// ///
/// Useful for deeplinks /// Useful for deep-links
final List<String> customLinkPrefixes; final List<String> customLinkPrefixes;
/// Configures the dialog theme. /// Configures the dialog theme.
@ -367,10 +366,10 @@ class QuillEditorConfigurations extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
placeholder, placeholder,
readOnly, controller.readOnly,
]; ];
// We might use code generator like freezed but sometimes it can be limitied // We might use code generator like freezed but sometimes it can be limited
// instead whatever there is a change to the parameters in this class please // instead whatever there is a change to the parameters in this class please
// regenerate this function using extension in vs code or plugin in intellij // regenerate this function using extension in vs code or plugin in intellij
@ -431,7 +430,6 @@ class QuillEditorConfigurations extends Equatable {
sharedConfigurations: sharedConfigurations ?? this.sharedConfigurations, sharedConfigurations: sharedConfigurations ?? this.sharedConfigurations,
controller: controller ?? this.controller, controller: controller ?? this.controller,
placeholder: placeholder ?? this.placeholder, placeholder: placeholder ?? this.placeholder,
readOnly: readOnly ?? this.readOnly,
checkBoxReadOnly: checkBoxReadOnly ?? this.checkBoxReadOnly, checkBoxReadOnly: checkBoxReadOnly ?? this.checkBoxReadOnly,
disableClipboard: disableClipboard ?? this.disableClipboard, disableClipboard: disableClipboard ?? this.disableClipboard,
scrollable: scrollable ?? this.scrollable, scrollable: scrollable ?? this.scrollable,

@ -1,3 +1,4 @@
export 'editor/editor_configurations.dart'; export 'editor/editor_configurations.dart';
export 'quill_controller_configurations.dart';
export 'quill_shared_configurations.dart'; export 'quill_shared_configurations.dart';
export 'toolbar/simple_toolbar_configurations.dart'; export 'toolbar/simple_toolbar_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<bool> Function()? onClipboardPaste;
}

@ -75,6 +75,9 @@ class QuillSimpleToolbarButtonOptions extends Equatable {
this.linkStyle = const QuillToolbarLinkStyleButtonOptions(), this.linkStyle = const QuillToolbarLinkStyleButtonOptions(),
this.linkStyle2 = const QuillToolbarLinkStyleButton2Options(), this.linkStyle2 = const QuillToolbarLinkStyleButton2Options(),
this.customButtons = const QuillToolbarCustomButtonOptions(), this.customButtons = const QuillToolbarCustomButtonOptions(),
this.clipboardCut = const QuillToolbarToggleStyleButtonOptions(),
this.clipboardCopy = const QuillToolbarToggleStyleButtonOptions(),
this.clipboardPaste = const QuillToolbarToggleStyleButtonOptions(),
}); });
/// The base configurations for all the buttons which will apply to all /// The base configurations for all the buttons which will apply to all
@ -113,6 +116,10 @@ class QuillSimpleToolbarButtonOptions extends Equatable {
final QuillToolbarSearchButtonOptions search; final QuillToolbarSearchButtonOptions search;
final QuillToolbarToggleStyleButtonOptions clipboardCut;
final QuillToolbarToggleStyleButtonOptions clipboardCopy;
final QuillToolbarToggleStyleButtonOptions clipboardPaste;
/// The reason we call this buttons in the end because this is responsible /// The reason we call this buttons in the end because this is responsible
/// for all the header style buttons and not just one, you still /// for all the header style buttons and not just one, you still
/// can customize it and you also have child builder /// can customize it and you also have child builder

@ -107,6 +107,9 @@ class QuillSimpleToolbarConfigurations extends QuillSharedToolbarProperties {
this.showSearchButton = true, this.showSearchButton = true,
this.showSubscript = true, this.showSubscript = true,
this.showSuperscript = true, this.showSuperscript = true,
this.showClipboardCut = true,
this.showClipboardCopy = true,
this.showClipboardPaste = true,
this.linkStyleType = LinkStyleType.original, this.linkStyleType = LinkStyleType.original,
this.headerStyleType = HeaderStyleType.original, this.headerStyleType = HeaderStyleType.original,
@ -195,6 +198,9 @@ class QuillSimpleToolbarConfigurations extends QuillSharedToolbarProperties {
final bool showSearchButton; final bool showSearchButton;
final bool showSubscript; final bool showSubscript;
final bool showSuperscript; final bool showSuperscript;
final bool showClipboardCut;
final bool showClipboardCopy;
final bool showClipboardPaste;
/// Toolbar items to display for controls of embed blocks /// Toolbar items to display for controls of embed blocks
final List<EmbedButtonBuilder>? embedButtons; final List<EmbedButtonBuilder>? embedButtons;

@ -407,20 +407,20 @@ base class Line extends QuillContainer<Leaf?> {
final data = queryChild(offset, true); final data = queryChild(offset, true);
var node = data.node as Leaf?; var node = data.node as Leaf?;
if (node != null) { if (node != null) {
var pos = 0; var pos = math.min(local, node.length - data.offset);
pos = node.length - data.offset;
if (node is QuillText && node.style.isNotEmpty) { if (node is QuillText && node.style.isNotEmpty) {
result.add(OffsetValue(beg, node.style, node.length)); result.add(OffsetValue(beg, node.style, pos));
} else if (node.value is Embeddable) { } else if (node.value is Embeddable) {
result.add(OffsetValue(beg, node.value as Embeddable, node.length)); result.add(OffsetValue(beg, node.value as Embeddable, pos));
} }
while (!node!.isLast && pos < local) { while (!node!.isLast && pos < local) {
node = node.next as Leaf; node = node.next as Leaf;
final span = math.min(local - pos, node.length);
if (node is QuillText && node.style.isNotEmpty) { if (node is QuillText && node.style.isNotEmpty) {
result.add(OffsetValue(pos + beg, node.style, node.length)); result.add(OffsetValue(pos + beg, node.style, span));
} else if (node.value is Embeddable) { } else if (node.value is Embeddable) {
result.add( result.add(OffsetValue(pos + beg, node.value as Embeddable, span));
OffsetValue(pos + beg, node.value as Embeddable, node.length));
} }
pos += node.length; pos += node.length;
} }

@ -2,17 +2,13 @@ import 'dart:math' as math;
import 'package:flutter/services.dart' show ClipboardData, Clipboard; import 'package:flutter/services.dart' show ClipboardData, Clipboard;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:super_clipboard/super_clipboard.dart';
import '../../../flutter_quill.dart';
import '../../../quill_delta.dart'; import '../../../quill_delta.dart';
import '../../models/documents/attribute.dart'; import '../../models/documents/delta_x.dart';
import '../../models/documents/document.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart';
import '../../models/documents/style.dart';
import '../../models/structs/doc_change.dart';
import '../../models/structs/image_url.dart';
import '../../models/structs/offset_value.dart';
import '../../utils/delta.dart'; import '../../utils/delta.dart';
typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); typedef ReplaceTextCallback = bool Function(int index, int len, Object? data);
@ -22,21 +18,28 @@ class QuillController extends ChangeNotifier {
QuillController({ QuillController({
required Document document, required Document document,
required TextSelection selection, required TextSelection selection,
this.configurations = const QuillControllerConfigurations(),
this.keepStyleOnNewLine = true, this.keepStyleOnNewLine = true,
this.onReplaceText, this.onReplaceText,
this.onDelete, this.onDelete,
this.onSelectionCompleted, this.onSelectionCompleted,
this.onSelectionChanged, this.onSelectionChanged,
this.readOnly = false,
}) : _document = document, }) : _document = document,
_selection = selection; _selection = selection;
factory QuillController.basic() { factory QuillController.basic(
{QuillControllerConfigurations configurations =
const QuillControllerConfigurations()}) {
return QuillController( return QuillController(
configurations: configurations,
document: Document(), document: Document(),
selection: const TextSelection.collapsed(offset: 0), selection: const TextSelection.collapsed(offset: 0),
); );
} }
final QuillControllerConfigurations configurations;
/// Document managed by this controller. /// Document managed by this controller.
Document _document; Document _document;
@ -471,9 +474,18 @@ class QuillController extends ChangeNotifier {
return document.querySegmentLeafNode(offset).leaf; return document.querySegmentLeafNode(offset).leaf;
} }
/// Clipboard for image url and its corresponding style // Notify toolbar buttons directly with attributes
ImageUrl? _copiedImageUrl; Map<String, Attribute> toolbarButtonToggler = const {};
/// Clipboard caches last copy to allow paste with styles. Static to allow paste between multiple instances of editor.
static String _pastePlainText = '';
static List<OffsetValue> _pasteStyleAndEmbed = <OffsetValue>[];
String get pastePlainText => _pastePlainText;
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed;
bool readOnly;
ImageUrl? _copiedImageUrl;
ImageUrl? get copiedImageUrl => _copiedImageUrl; ImageUrl? get copiedImageUrl => _copiedImageUrl;
set copiedImageUrl(ImageUrl? value) { set copiedImageUrl(ImageUrl? value) {
@ -481,6 +493,138 @@ class QuillController extends ChangeNotifier {
Clipboard.setData(const ClipboardData(text: '')); Clipboard.setData(const ClipboardData(text: ''));
} }
// Notify toolbar buttons directly with attributes bool clipboardSelection(bool copy) {
Map<String, Attribute> toolbarButtonToggler = const {}; copiedImageUrl = null;
_pastePlainText = getPlainText();
_pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed();
if (!selection.isCollapsed) {
Clipboard.setData(ClipboardData(text: _pastePlainText));
if (!copy) {
if (readOnly) return false;
final sel = selection;
replaceText(sel.start, sel.end - sel.start, '',
TextSelection.collapsed(offset: sel.start));
}
return true;
}
return false;
}
/// Returns whether paste operation was handled here.
/// updateEditor is called if paste operation was successful.
Future<bool> clipboardPaste({void Function()? updateEditor}) async {
if (readOnly || !selection.isValid) return true;
if (await _pasteHTML()) {
updateEditor?.call();
return true;
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final plainText = await Clipboard.getData(Clipboard.kTextPlain);
if (plainText != null) {
replaceTextWithEmbeds(
selection.start,
selection.end - selection.start,
plainText.text!,
TextSelection.collapsed(
offset: selection.start + plainText.text!.length),
);
updateEditor?.call();
return true;
}
if (await configurations.onClipboardPaste?.call() == true) {
updateEditor?.call();
return true;
}
return false;
}
Future<bool> _pasteHTML() async {
final clipboard = SystemClipboard.instance;
if (clipboard != null) {
final reader = await clipboard.read();
if (reader.canProvide(Formats.htmlText)) {
final html = await reader.readValue(Formats.htmlText);
if (html == null) {
return false;
}
final htmlBody = html_parser.parse(html).body?.outerHtml;
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? html);
replaceText(
selection.start,
selection.end - selection.start,
deltaFromClipboard,
TextSelection.collapsed(offset: selection.end),
);
return true;
}
}
return false;
}
void replaceTextWithEmbeds(
int index,
int len,
String insertedText,
TextSelection? textSelection, {
bool ignoreFocus = false,
bool shouldNotifyListeners = true,
}) {
final containsEmbed =
insertedText.codeUnits.contains(Embed.kObjectReplacementInt);
insertedText =
containsEmbed ? _adjustInsertedText(insertedText) : insertedText;
replaceText(index, len, insertedText, textSelection,
ignoreFocus: ignoreFocus, shouldNotifyListeners: shouldNotifyListeners);
_applyPasteStyleAndEmbed(insertedText, index, containsEmbed);
}
void _applyPasteStyleAndEmbed(
String insertedText, int start, bool containsEmbed) {
if (insertedText == pastePlainText && pastePlainText != '' ||
containsEmbed) {
final pos = start;
for (final p in pasteStyleAndEmbed) {
final offset = p.offset;
final styleAndEmbed = p.value;
final local = pos + offset;
if (styleAndEmbed is Embeddable) {
replaceText(local, 0, styleAndEmbed, null);
} else {
final style = styleAndEmbed as Style;
if (style.isInline) {
formatTextStyle(local, p.length!, style);
} else if (style.isBlock) {
final node = document.queryChild(local).node;
if (node != null && p.length == node.length - 1) {
for (final attribute in style.values) {
document.format(local, 0, attribute);
}
}
}
}
}
}
}
String _adjustInsertedText(String text) {
final sb = StringBuffer();
for (var i = 0; i < text.length; i++) {
if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) {
continue;
}
sb.write(text[i]);
}
return sb.toString();
}
} }

@ -13,17 +13,15 @@ import 'package:flutter/services.dart'
Clipboard, Clipboard,
ClipboardData, ClipboardData,
HardwareKeyboard, HardwareKeyboard,
LogicalKeyboardKey,
KeyDownEvent, KeyDownEvent,
LogicalKeyboardKey,
SystemChannels, SystemChannels,
TextInputControl; TextInputControl;
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart' import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'
show KeyboardVisibilityController; show KeyboardVisibilityController;
import 'package:html/parser.dart' as html_parser;
import 'package:super_clipboard/super_clipboard.dart'; import 'package:super_clipboard/super_clipboard.dart';
import '../../models/documents/attribute.dart'; import '../../models/documents/attribute.dart';
import '../../models/documents/delta_x.dart';
import '../../models/documents/document.dart'; import '../../models/documents/document.dart';
import '../../models/documents/nodes/block.dart'; import '../../models/documents/nodes/block.dart';
import '../../models/documents/nodes/embeddable.dart'; import '../../models/documents/nodes/embeddable.dart';
@ -92,12 +90,10 @@ class QuillRawEditorState extends EditorState
// for pasting style // for pasting style
@override @override
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed; List<OffsetValue> get pasteStyleAndEmbed => controller.pasteStyleAndEmbed;
List<OffsetValue> _pasteStyleAndEmbed = <OffsetValue>[];
@override @override
String get pastePlainText => _pastePlainText; String get pastePlainText => controller.pastePlainText;
String _pastePlainText = '';
ClipboardStatusNotifier? _clipboardStatus; ClipboardStatusNotifier? _clipboardStatus;
final LayerLink _toolbarLayerLink = LayerLink(); final LayerLink _toolbarLayerLink = LayerLink();
@ -122,16 +118,7 @@ class QuillRawEditorState extends EditorState
/// Copy current selection to [Clipboard]. /// Copy current selection to [Clipboard].
@override @override
void copySelection(SelectionChangedCause cause) { void copySelection(SelectionChangedCause cause) {
controller.copiedImageUrl = null; if (!controller.clipboardSelection(true)) return;
_pastePlainText = controller.getPlainText();
_pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed();
final selection = textEditingValue.selection;
final text = textEditingValue.text;
if (selection.isCollapsed) {
return;
}
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent); bringIntoView(textEditingValue.selection.extent);
@ -152,20 +139,7 @@ class QuillRawEditorState extends EditorState
/// Cut current selection to [Clipboard]. /// Cut current selection to [Clipboard].
@override @override
void cutSelection(SelectionChangedCause cause) { void cutSelection(SelectionChangedCause cause) {
controller.copiedImageUrl = null; if (!controller.clipboardSelection(false)) return;
_pastePlainText = controller.getPlainText();
_pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed();
if (widget.configurations.readOnly) {
return;
}
final selection = textEditingValue.selection;
final text = textEditingValue.text;
if (selection.isCollapsed) {
return;
}
Clipboard.setData(ClipboardData(text: selection.textInside(text)));
_replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause));
if (cause == SelectionChangedCause.toolbar) { if (cause == SelectionChangedCause.toolbar) {
bringIntoView(textEditingValue.selection.extent); bringIntoView(textEditingValue.selection.extent);
@ -176,7 +150,7 @@ class QuillRawEditorState extends EditorState
/// Paste text from [Clipboard]. /// Paste text from [Clipboard].
@override @override
Future<void> pasteText(SelectionChangedCause cause) async { Future<void> pasteText(SelectionChangedCause cause) async {
if (widget.configurations.readOnly) { if (controller.readOnly) {
return; return;
} }
@ -205,76 +179,13 @@ class QuillRawEditorState extends EditorState
return; return;
} }
final selection = textEditingValue.selection; if (await controller.clipboardPaste()) {
if (!selection.isValid) { bringIntoView(textEditingValue.selection.extent);
return; return;
} }
final clipboard = SystemClipboard.instance; final clipboard = SystemClipboard.instance;
if (clipboard != null) {
final reader = await clipboard.read();
if (reader.canProvide(Formats.htmlText)) {
final html = await reader.readValue(Formats.htmlText);
if (html == null) {
return;
}
final htmlBody = html_parser.parse(html).body?.outerHtml;
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? html);
controller.replaceText(
textEditingValue.selection.start,
textEditingValue.selection.end - textEditingValue.selection.start,
deltaFromClipboard,
TextSelection.collapsed(offset: textEditingValue.selection.end),
);
bringIntoView(textEditingValue.selection.extent);
// Collapse the selection and hide the toolbar and handles.
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection: TextSelection.collapsed(
offset: textEditingValue.selection.end,
),
),
cause,
);
return;
}
}
// Snapshot the input before using `await`.
// See https://github.com/flutter/flutter/issues/11427
final plainText = await Clipboard.getData(Clipboard.kTextPlain);
if (plainText != null) {
_replaceText(
ReplaceTextIntent(
textEditingValue,
plainText.text!,
selection,
cause,
),
);
bringIntoView(textEditingValue.selection.extent);
// Collapse the selection and hide the toolbar and handles.
userUpdateTextEditingValue(
TextEditingValue(
text: textEditingValue.text,
selection: TextSelection.collapsed(
offset: textEditingValue.selection.end,
),
),
cause,
);
return;
}
final onImagePaste = widget.configurations.onImagePaste; final onImagePaste = widget.configurations.onImagePaste;
if (onImagePaste != null) { if (onImagePaste != null) {
if (clipboard != null) { if (clipboard != null) {
@ -322,7 +233,6 @@ class QuillRawEditorState extends EditorState
} }
} }
} }
return;
} }
/// Select the entire text value. /// Select the entire text value.

@ -4,9 +4,6 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../../models/documents/document.dart'; import '../../models/documents/document.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart';
import '../../models/documents/style.dart';
import '../../utils/delta.dart'; import '../../utils/delta.dart';
import 'raw_editor.dart'; import 'raw_editor.dart';
@ -29,62 +26,8 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
return; return;
} }
var insertedText = diff.inserted; widget.configurations.controller.replaceTextWithEmbeds(
final containsEmbed = diff.start, diff.deleted.length, diff.inserted, value.selection);
insertedText.codeUnits.contains(Embed.kObjectReplacementInt);
insertedText =
containsEmbed ? _adjustInsertedText(diff.inserted) : diff.inserted;
widget.configurations.controller.replaceText(
diff.start, diff.deleted.length, insertedText, value.selection);
_applyPasteStyleAndEmbed(insertedText, diff.start, containsEmbed);
}
void _applyPasteStyleAndEmbed(
String insertedText, int start, bool containsEmbed) {
if (insertedText == pastePlainText && pastePlainText != '' ||
containsEmbed) {
final pos = start;
for (var i = 0; i < pasteStyleAndEmbed.length; i++) {
final offset = pasteStyleAndEmbed[i].offset;
final styleAndEmbed = pasteStyleAndEmbed[i].value;
final local = pos + offset;
if (styleAndEmbed is Embeddable) {
widget.configurations.controller
.replaceText(local, 0, styleAndEmbed, null);
} else {
final style = styleAndEmbed as Style;
if (style.isInline) {
widget.configurations.controller
.formatTextStyle(local, pasteStyleAndEmbed[i].length!, style);
} else if (style.isBlock) {
final node = widget.configurations.controller.document
.queryChild(local)
.node;
if (node != null &&
pasteStyleAndEmbed[i].length == node.length - 1) {
for (final attribute in style.values) {
widget.configurations.controller.document
.format(local, 0, attribute);
}
}
}
}
}
}
}
String _adjustInsertedText(String text) {
final sb = StringBuffer();
for (var i = 0; i < text.length; i++) {
if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) {
continue;
}
sb.write(text[i]);
}
return sb.toString();
} }
@override @override

@ -26,7 +26,9 @@ abstract class QuillToolbarBaseValueButtonState<
QuillController get controller => widget.controller; QuillController get controller => widget.controller;
late V currentValue; V? _currentValue;
V get currentValue => _currentValue!;
set currentValue(V value) => _currentValue = value;
/// Callback to query the widget's state for the value to be assigned to currentState /// Callback to query the widget's state for the value to be assigned to currentState
V get currentStateValue; V get currentStateValue;
@ -35,6 +37,7 @@ abstract class QuillToolbarBaseValueButtonState<
void initState() { void initState() {
super.initState(); super.initState();
controller.addListener(didChangeEditingValue); controller.addListener(didChangeEditingValue);
addExtraListener();
} }
@override @override
@ -50,6 +53,7 @@ abstract class QuillToolbarBaseValueButtonState<
@override @override
void dispose() { void dispose() {
controller.removeListener(didChangeEditingValue); controller.removeListener(didChangeEditingValue);
removeExtraListener(widget);
super.dispose(); super.dispose();
} }
@ -58,11 +62,17 @@ abstract class QuillToolbarBaseValueButtonState<
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.controller != controller) { if (oldWidget.controller != controller) {
oldWidget.controller.removeListener(didChangeEditingValue); oldWidget.controller.removeListener(didChangeEditingValue);
removeExtraListener(oldWidget);
controller.addListener(didChangeEditingValue); controller.addListener(didChangeEditingValue);
addExtraListener();
currentValue = currentStateValue; currentValue = currentStateValue;
} }
} }
/// Extra listeners allow a subclass to listen to an external event that can affect its currentValue
void addExtraListener() {}
void removeExtraListener(covariant W oldWidget) {}
String get defaultTooltip; String get defaultTooltip;
String get tooltip { String get tooltip {
@ -96,3 +106,12 @@ abstract class QuillToolbarBaseValueButtonState<
baseButtonExtraOptions?.afterButtonPressed; baseButtonExtraOptions?.afterButtonPressed;
} }
} }
typedef QuillToolbarToggleStyleBaseButton = QuillToolbarBaseValueButton<
QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions>;
typedef QuillToolbarToggleStyleBaseButtonState<
W extends QuillToolbarToggleStyleBaseButton>
= QuillToolbarBaseValueButtonState<W, QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions, bool>;

@ -10,6 +10,7 @@ import 'simple_toolbar.dart';
export '../../models/config/toolbar/base_button_configurations.dart'; export '../../models/config/toolbar/base_button_configurations.dart';
export '../../models/config/toolbar/simple_toolbar_configurations.dart'; export '../../models/config/toolbar/simple_toolbar_configurations.dart';
export 'buttons/clear_format_button.dart'; export 'buttons/clear_format_button.dart';
export 'buttons/clipboard_button.dart';
export 'buttons/color/color_button.dart'; export 'buttons/color/color_button.dart';
export 'buttons/custom_button_button.dart'; export 'buttons/custom_button_button.dart';
export 'buttons/font_family_button.dart'; export 'buttons/font_family_button.dart';

@ -0,0 +1,144 @@
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';
import '../../../l10n/extensions/localizations.dart';
import '../base_button/base_value_button.dart';
enum ClipboardAction { cut, copy, paste }
class ClipboardMonitor {
bool _canPaste = false;
bool get canPaste => _canPaste;
Timer? _timer;
void monitorClipboard(bool add, void Function() listener) {
if (kIsWeb) return;
if (add) {
_timer = Timer.periodic(
const Duration(seconds: 1), (timer) => _update(listener));
} else {
_timer?.cancel();
}
}
Future<void> _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();
}
}
}
}
class QuillToolbarClipboardButton extends QuillToolbarToggleStyleBaseButton {
QuillToolbarClipboardButton(
{required super.controller,
required this.clipboardAction,
super.options = const QuillToolbarToggleStyleButtonOptions(),
super.key});
final ClipboardAction clipboardAction;
final ClipboardMonitor _monitor = ClipboardMonitor();
@override
State<StatefulWidget> createState() => QuillToolbarClipboardButtonState();
}
class QuillToolbarClipboardButtonState
extends QuillToolbarToggleStyleBaseButtonState<
QuillToolbarClipboardButton> {
@override
bool get currentStateValue {
switch (widget.clipboardAction) {
case ClipboardAction.cut:
return !controller.readOnly && !controller.selection.isCollapsed;
case ClipboardAction.copy:
return !controller.selection.isCollapsed;
case ClipboardAction.paste:
return !controller.readOnly && (kIsWeb || widget._monitor.canPaste);
}
}
void _listenClipboardStatus() => didChangeEditingValue();
@override
void addExtraListener() {
if (widget.clipboardAction == ClipboardAction.paste) {
widget._monitor.monitorClipboard(true, _listenClipboardStatus);
}
}
@override
void removeExtraListener(covariant QuillToolbarClipboardButton oldWidget) {
if (widget.clipboardAction == ClipboardAction.paste) {
oldWidget._monitor.monitorClipboard(false, _listenClipboardStatus);
}
}
@override
String get defaultTooltip => switch (widget.clipboardAction) {
ClipboardAction.cut => context.loc.cut,
ClipboardAction.copy => context.loc.copy,
ClipboardAction.paste => context.loc.paste,
};
IconData get _icon => switch (widget.clipboardAction) {
ClipboardAction.cut => Icons.cut_outlined,
ClipboardAction.copy => Icons.copy_outlined,
ClipboardAction.paste => Icons.paste_outlined,
};
void _onPressed() {
switch (widget.clipboardAction) {
case ClipboardAction.cut:
controller.clipboardSelection(false);
break;
case ClipboardAction.copy:
controller.clipboardSelection(true);
break;
case ClipboardAction.paste:
controller.clipboardPaste();
break;
}
afterButtonPressed?.call();
}
@override
Widget build(BuildContext context) {
final childBuilder = options.childBuilder ??
context.quillToolbarBaseButtonOptions?.childBuilder;
if (childBuilder != null) {
return childBuilder(
options,
QuillToolbarToggleStyleButtonExtraOptions(
context: context,
controller: controller,
onPressed: _onPressed,
isToggled: currentValue,
),
);
}
return UtilityWidgets.maybeTooltip(
message: tooltip,
child: QuillToolbarIconButton(
icon: Icon(
_icon,
size: iconSize * iconButtonFactor,
),
isSelected: false,
onPressed: currentValue ? _onPressed : null,
afterPressed: afterButtonPressed,
iconTheme: iconTheme,
));
}
}

@ -20,9 +20,7 @@ typedef ToggleStyleButtonBuilder = Widget Function(
QuillIconTheme? iconTheme, QuillIconTheme? iconTheme,
]); ]);
class QuillToolbarToggleStyleButton extends QuillToolbarBaseValueButton< class QuillToolbarToggleStyleButton extends QuillToolbarToggleStyleBaseButton {
QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions> {
const QuillToolbarToggleStyleButton({ const QuillToolbarToggleStyleButton({
required super.controller, required super.controller,
required this.attribute, required this.attribute,
@ -38,11 +36,8 @@ class QuillToolbarToggleStyleButton extends QuillToolbarBaseValueButton<
} }
class QuillToolbarToggleStyleButtonState class QuillToolbarToggleStyleButtonState
extends QuillToolbarBaseValueButtonState< extends QuillToolbarToggleStyleBaseButtonState<
QuillToolbarToggleStyleButton, QuillToolbarToggleStyleButton> {
QuillToolbarToggleStyleButtonOptions,
QuillToolbarToggleStyleButtonExtraOptions,
bool> {
Style get _selectionStyle => controller.getSelectionStyle(); Style get _selectionStyle => controller.getSelectionStyle();
@override @override

@ -18,6 +18,8 @@ class QuillSimpleToolbar extends StatelessWidget
/// The configurations for the toolbar widget of flutter quill /// The configurations for the toolbar widget of flutter quill
final QuillSimpleToolbarConfigurations configurations; final QuillSimpleToolbarConfigurations configurations;
double get _toolbarSize => configurations.toolbarSize * 1.4;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theEmbedButtons = configurations.embedButtons; final theEmbedButtons = configurations.embedButtons;
@ -58,6 +60,14 @@ class QuillSimpleToolbar extends StatelessWidget
final axis = toolbarConfigurations.axis; final axis = toolbarConfigurations.axis;
final globalController = configurations.controller; final globalController = configurations.controller;
final divider = SizedBox(
height: _toolbarSize,
child: QuillToolbarDivider(
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
));
return [ return [
if (configurations.showUndo) if (configurations.showUndo)
QuillToolbarHistoryButton( QuillToolbarHistoryButton(
@ -160,11 +170,7 @@ class QuillSimpleToolbar extends StatelessWidget
isButtonGroupShown[3] || isButtonGroupShown[3] ||
isButtonGroupShown[4] || isButtonGroupShown[4] ||
isButtonGroupShown[5])) isButtonGroupShown[5]))
QuillToolbarDivider( divider,
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
if (configurations.showAlignmentButtons) if (configurations.showAlignmentButtons)
QuillToolbarSelectAlignmentButtons( QuillToolbarSelectAlignmentButtons(
controller: globalController, controller: globalController,
@ -188,11 +194,7 @@ class QuillSimpleToolbar extends StatelessWidget
isButtonGroupShown[3] || isButtonGroupShown[3] ||
isButtonGroupShown[4] || isButtonGroupShown[4] ||
isButtonGroupShown[5])) isButtonGroupShown[5]))
QuillToolbarDivider( divider,
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
if (configurations.showHeaderStyle) ...[ if (configurations.showHeaderStyle) ...[
if (configurations.headerStyleType.isOriginal) if (configurations.headerStyleType.isOriginal)
QuillToolbarSelectHeaderStyleDropdownButton( QuillToolbarSelectHeaderStyleDropdownButton(
@ -213,11 +215,7 @@ class QuillSimpleToolbar extends StatelessWidget
(isButtonGroupShown[3] || (isButtonGroupShown[3] ||
isButtonGroupShown[4] || isButtonGroupShown[4] ||
isButtonGroupShown[5])) isButtonGroupShown[5]))
QuillToolbarDivider( divider,
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
if (configurations.showListNumbers) if (configurations.showListNumbers)
QuillToolbarToggleStyleButton( QuillToolbarToggleStyleButton(
attribute: Attribute.ol, attribute: Attribute.ol,
@ -244,11 +242,7 @@ class QuillSimpleToolbar extends StatelessWidget
if (configurations.showDividers && if (configurations.showDividers &&
isButtonGroupShown[3] && isButtonGroupShown[3] &&
(isButtonGroupShown[4] || isButtonGroupShown[5])) ...[ (isButtonGroupShown[4] || isButtonGroupShown[5])) ...[
QuillToolbarDivider( divider,
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
], ],
if (configurations.showQuote) if (configurations.showQuote)
QuillToolbarToggleStyleButton( QuillToolbarToggleStyleButton(
@ -271,11 +265,7 @@ class QuillSimpleToolbar extends StatelessWidget
if (configurations.showDividers && if (configurations.showDividers &&
isButtonGroupShown[4] && isButtonGroupShown[4] &&
isButtonGroupShown[5]) isButtonGroupShown[5])
QuillToolbarDivider( divider,
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
if (configurations.showLink) if (configurations.showLink)
toolbarConfigurations.linkStyleType.isOriginal toolbarConfigurations.linkStyleType.isOriginal
? QuillToolbarLinkStyleButton( ? QuillToolbarLinkStyleButton(
@ -291,13 +281,26 @@ class QuillSimpleToolbar extends StatelessWidget
controller: globalController, controller: globalController,
options: toolbarConfigurations.buttonOptions.search, options: toolbarConfigurations.buttonOptions.search,
), ),
if (configurations.showClipboardCut)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardCut,
controller: globalController,
clipboardAction: ClipboardAction.cut,
),
if (configurations.showClipboardCopy)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardCopy,
controller: globalController,
clipboardAction: ClipboardAction.copy,
),
if (configurations.showClipboardPaste)
QuillToolbarClipboardButton(
options: toolbarConfigurations.buttonOptions.clipboardPaste,
controller: globalController,
clipboardAction: ClipboardAction.paste,
),
if (configurations.customButtons.isNotEmpty) ...[ if (configurations.customButtons.isNotEmpty) ...[
if (configurations.showDividers) if (configurations.showDividers) divider,
QuillToolbarDivider(
axis,
color: configurations.sectionDividerColor,
space: configurations.sectionDividerSpace,
),
for (final customButton in configurations.customButtons) for (final customButton in configurations.customButtons)
QuillToolbarCustomButton( QuillToolbarCustomButton(
options: customButton, options: customButton,
@ -347,11 +350,10 @@ class QuillSimpleToolbar extends StatelessWidget
), ),
constraints: BoxConstraints.tightFor( constraints: BoxConstraints.tightFor(
height: configurations.axis == Axis.horizontal height: configurations.axis == Axis.horizontal
? configurations.toolbarSize ? _toolbarSize
: null,
width: configurations.axis == Axis.vertical
? configurations.toolbarSize
: null, : null,
width:
configurations.axis == Axis.vertical ? _toolbarSize : null,
), ),
child: QuillToolbarArrowIndicatedButtonList( child: QuillToolbarArrowIndicatedButtonList(
axis: configurations.axis, axis: configurations.axis,

@ -65,7 +65,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
), ),
); );
}); });

@ -6,6 +6,7 @@ import 'package:test/test.dart';
void main() { void main() {
const testDocumentContents = 'data'; const testDocumentContents = 'data';
late QuillController controller; late QuillController controller;
WidgetsFlutterBinding.ensureInitialized();
setUp(() { setUp(() {
controller = QuillController.basic() controller = QuillController.basic()
@ -119,6 +120,26 @@ void main() {
expect((result[1].value as Embeddable).type, BlockEmbed.imageType); expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
}); });
test('getAllIndividualSelectionStylesAndEmbed mixed', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
expect(controller.getPlainText(), 'ld plain ita',
reason: 'Selection spans 3 styles');
//
final result = controller.getAllIndividualSelectionStylesAndEmbed();
expect(result.length, 2);
expect(result[0].offset, 0);
expect(result[0].length, 2, reason: 'First style is 2 characters bold');
expect(result[0].value, const Style().put(Attribute.bold));
expect(result[1].offset, 9);
expect(result[1].length, 3, reason: 'Last style is 3 characters italic');
expect(result[1].value, const Style().put(Attribute.italic));
});
test('getPlainText', () { test('getPlainText', () {
controller.updateSelection( controller.updateSelection(
const TextSelection(baseOffset: 0, extentOffset: 4), const TextSelection(baseOffset: 0, extentOffset: 4),
@ -302,5 +323,37 @@ void main() {
expect(controller.document.toDelta(), expect(controller.document.toDelta(),
Delta()..insert('test $originalContents')); Delta()..insert('test $originalContents'));
}); });
test('clipboardSelection empty', () {
expect(controller.clipboardSelection(true), false,
reason: 'No effect when no selection');
expect(controller.clipboardSelection(false), false);
});
test('clipboardSelection', () {
controller
..replaceText(0, 4, 'bold plain italic', null)
..formatText(0, 4, Attribute.bold)
..formatText(11, 17, Attribute.italic)
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 14),
ChangeSource.local);
//
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 18,
reason: 'Copy does not change the document');
expect(controller.clipboardSelection(false), true);
expect(controller.document.length, 6, reason: 'Cut changes the document');
//
controller
..readOnly = true
..updateSelection(const TextSelection(baseOffset: 2, extentOffset: 4),
ChangeSource.local);
expect(controller.selection.isCollapsed, false);
expect(controller.clipboardSelection(true), true);
expect(controller.document.length, 6);
expect(controller.clipboardSelection(false), false);
expect(controller.document.length, 6,
reason: 'Cut not permitted on readOnly document');
});
}); });
} }

@ -27,7 +27,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
), ),
), ),
), ),
@ -47,7 +46,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
autoFocus: true, autoFocus: true,
expands: true, expands: true,
contentInsertionConfiguration: ContentInsertionConfiguration( contentInsertionConfiguration: ContentInsertionConfiguration(
@ -121,7 +119,6 @@ void main() {
configurations: QuillEditorConfigurations( configurations: QuillEditorConfigurations(
controller: controller, controller: controller,
// ignore: avoid_redundant_argument_values // ignore: avoid_redundant_argument_values
readOnly: false,
autoFocus: true, autoFocus: true,
expands: true, expands: true,
contextMenuBuilder: customBuilder, contextMenuBuilder: customBuilder,

Loading…
Cancel
Save