Tests Paste multiline selection

pull/2074/head
AtlasAutocode 9 months ago
parent b8f9ff58f6
commit f7f5c32b37
  1. 63
      lib/src/controller/quill_controller.dart
  2. 11
      lib/src/controller/quill_controller_configurations.dart
  3. 4
      lib/src/delta/delta_diff.dart
  4. 190
      test/controller/controller_clipboard_test.dart
  5. 32
      test/controller/controller_test.dart

@ -40,19 +40,25 @@ class QuillController extends ChangeNotifier {
_selection = selection;
factory QuillController.basic(
{QuillControllerConfigurations configurations =
const QuillControllerConfigurations(),
FocusNode? editorFocusNode}) {
return QuillController(
configurations: configurations,
editorFocusNode: editorFocusNode,
document: Document(),
selection: const TextSelection.collapsed(offset: 0),
);
}
{QuillControllerConfigurations configurations =
const QuillControllerConfigurations(),
FocusNode? editorFocusNode}) =>
QuillController(
configurations: configurations,
editorFocusNode: editorFocusNode,
document: Document(),
selection: const TextSelection.collapsed(offset: 0),
);
final QuillControllerConfigurations configurations;
/// Local copy of editor configurations enables fail-safe setting from editor _initState method
QuillEditorConfigurations? _editorConfigurations;
QuillEditorConfigurations? get editorConfigurations =>
configurations.editorConfigurations ?? _editorConfigurations;
set editorConfigurations(QuillEditorConfigurations? value) =>
_editorConfigurations = value;
/// Document managed by this controller.
Document _document;
@ -403,7 +409,8 @@ class QuillController extends ChangeNotifier {
}
textSelection = selection.copyWith(
baseOffset: delta.transformPosition(selection.baseOffset, force: false),
baseOffset:
delta.transformPosition(selection.baseOffset, force: false),
extentOffset: delta.transformPosition(
selection.extentOffset,
force: false,
@ -489,9 +496,6 @@ class QuillController extends ChangeNotifier {
/// Used to give focus to the editor following a toolbar action
FocusNode? editorFocusNode;
/// Used to access embedBuilders for clipboard output
QuillEditorConfigurations? editorConfigurations;
ImageUrl? _copiedImageUrl;
ImageUrl? get copiedImageUrl => _copiedImageUrl;
@ -553,14 +557,7 @@ class QuillController extends ChangeNotifier {
// See https://github.com/flutter/flutter/issues/11427
final plainTextClipboardData =
await Clipboard.getData(Clipboard.kTextPlain);
if (plainTextClipboardData?.text != null) {
/// Internal copy-paste preserves styles and embeds
if ( plainTextClipboardData!.text == _pastePlainText && _pastePlainText.isNotEmpty && _pasteDelta.isNotEmpty ) {
replaceText(selection.start, selection.end - selection.start, _pasteDelta, TextSelection.collapsed(offset: selection.end));
} else {
replaceText(selection.start, selection.end - selection.start, plainTextClipboardData.text, TextSelection.collapsed(offset: selection.end + plainTextClipboardData.text!.length));
}
if (pasteUsingPlainOrDelta(plainTextClipboardData?.text)) {
updateEditor?.call();
return true;
}
@ -573,6 +570,28 @@ class QuillController extends ChangeNotifier {
return false;
}
/// Internal method to allow unit testing
bool pasteUsingPlainOrDelta(String? clipboardText) {
if (clipboardText != null) {
/// Internal copy-paste preserves styles and embeds
if (clipboardText == _pastePlainText &&
_pastePlainText.isNotEmpty &&
_pasteDelta.isNotEmpty) {
replaceText(selection.start, selection.end - selection.start,
_pasteDelta, TextSelection.collapsed(offset: selection.end));
} else {
replaceText(
selection.start,
selection.end - selection.start,
clipboardText,
TextSelection.collapsed(
offset: selection.end + clipboardText.length));
}
return true;
}
return false;
}
void _pasteUsingDelta(Delta deltaFromClipboard) {
replaceText(
selection.start,

@ -1,6 +1,15 @@
import '../editor/config/editor_configurations.dart';
class QuillControllerConfigurations {
const QuillControllerConfigurations(
{this.onClipboardPaste, this.requireScriptFontFeatures = false});
{this.editorConfigurations,
this.onClipboardPaste,
this.requireScriptFontFeatures = false});
/// Provides central access to editor configurations required for controller actions
///
/// Future: will be changed to 'required final'
final QuillEditorConfigurations? editorConfigurations;
/// Callback when the user pastes and data has not already been processed
///

@ -71,8 +71,8 @@ int getPositionDelta(Delta user, Delta actual) {
}
if (userOperation.key == actualOperation.key) {
/// Insertions must update diff allowing for type mismatch of Operation
if ( userOperation.key == Operation.insertKey) {
if ( userOperation.data is Delta && actualOperation.data is String ) {
if (userOperation.key == Operation.insertKey) {
if (userOperation.data is Delta && actualOperation.data is String) {
diff += actualOperation.length!;
}
}

@ -0,0 +1,190 @@
import 'package:flutter/material.dart';
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter_quill/quill_delta.dart';
import 'package:test/test.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
group('copy', () {
const testDocumentContents = 'data';
late QuillController controller;
setUp(() {
controller = QuillController.basic()
..compose(Delta()..insert(testDocumentContents),
const TextSelection.collapsed(offset: 0), ChangeSource.local);
});
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');
});
});
group('paste', () {
test('Plain', () async {
final controller = QuillController.basic()
..compose(Delta()..insert('[]'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(
const TextSelection.collapsed(offset: 1), ChangeSource.local);
//
expect(controller.document.toPlainText(), '[]\n');
expect(controller.pasteUsingPlainOrDelta('insert'), true);
expect(controller.document.toPlainText(), '[insert]\n');
});
test('Plain lines', () async {
final controller = QuillController.basic()
..compose(Delta()..insert('[]'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(
const TextSelection.collapsed(offset: 1), ChangeSource.local);
//
expect(controller.document.toPlainText(), '[]\n');
expect(controller.pasteUsingPlainOrDelta('1\n2\n3\n'), true);
expect(controller.document.toPlainText(), '[1\n2\n3\n]\n');
});
test('Paste from external', () async {
final source = QuillController.basic()
..compose(Delta()..insert('Plain text'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(const TextSelection(baseOffset: 4, extentOffset: 8),
ChangeSource.local);
assert(source.clipboardSelection(true));
expect(source.pastePlainText, 'n te');
//
final controller = QuillController.basic()
..compose(Delta()..insert('[]'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(
const TextSelection.collapsed(offset: 1), ChangeSource.local);
//
expect(controller.pasteUsingPlainOrDelta('insert'), true,
reason: 'External paste');
expect(controller.document.toPlainText(), '[insert]\n');
});
test('Delta simple', () async {
final source = QuillController.basic()
..compose(Delta()..insert('Plain text'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..formatText(6, 8, Attribute.bold)
..updateSelection(const TextSelection(baseOffset: 4, extentOffset: 8),
ChangeSource.local);
assert(source.clipboardSelection(true));
expect(source.pastePlainText, 'n te');
expect(
source.pasteDelta,
Delta()
..insert('n ')
..insert('te', {'bold': true}));
//
final controller = QuillController.basic()
..compose(Delta()..insert('[]'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(
const TextSelection.collapsed(offset: 1), ChangeSource.local);
//
expect(controller.pasteUsingPlainOrDelta('n te'), true,
reason: 'Internal paste');
expect(controller.document.toPlainText(), '[n te]\n');
expect(
controller.document.toDelta(),
Delta()
..insert('[n ')
..insert('te', {'bold': true})
..insert(']\n'));
expect(controller.selection, const TextSelection.collapsed(offset: 5));
});
test('Delta multi line', () async {
const blockAttribute = Attribute.ol;
const plainSelection = 'BC\nDEF\nGHI\nJK';
final source = QuillController.basic()
..compose(Delta()..insert('ABC\nDEF\nGHI\nJKL'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..formatText(1, 1, Attribute.underline) // ABC with B underlined
..formatText(4, 0, blockAttribute) // 1. DEF with E in italic
..formatText(5, 1, Attribute.italic)
..formatText(8, 0, blockAttribute) // 2. GHI with H as inline code
..formatText(9, 1, Attribute.inlineCode)
..formatText(13, 1, Attribute.strikeThrough) // JKL with K strikethrough
..updateSelection(const TextSelection(baseOffset: 1, extentOffset: 14),
ChangeSource.local);
//
assert(source.clipboardSelection(true));
expect(source.pastePlainText, plainSelection);
expect(
source.pasteDelta,
Delta()
..insert('B', {'underline': true})
..insert('C\nD')
..insert('E', {'italic': true})
..insert('F')
..insert('\n', {'list': 'ordered'})
..insert('G')
..insert('H', {'code': true})
..insert('I')
..insert('\n', {'list': 'ordered'})
..insert('J')
..insert('K', {'strike': true}));
//
final controller = QuillController.basic()
..compose(Delta()..insert('[]'),
const TextSelection.collapsed(offset: 0), ChangeSource.local)
..updateSelection(
const TextSelection.collapsed(offset: 1), ChangeSource.local);
//
expect(controller.pasteUsingPlainOrDelta(plainSelection), true,
reason: 'Internal paste');
expect(controller.document.toPlainText(), '[$plainSelection]\n');
expect(
controller.document.toDelta(),
Delta()
..insert('[')
..insert('B', {'underline': true})
..insert('C\nD')
..insert('E', {'italic': true})
..insert('F')
..insert('\n', {'list': 'ordered'})
..insert('G')
..insert('H', {'code': true})
..insert('I')
..insert('\n', {'list': 'ordered'})
..insert('J')
..insert('K', {'strike': true})
..insert(']\n'));
expect(controller.selection, const TextSelection.collapsed(offset: 14));
});
});
}

@ -324,38 +324,6 @@ void main() {
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');
});
test('blockSelectionStyles', () {
Style select(int start, int end) {
controller.updateSelection(

Loading…
Cancel
Save