commit
6978512b89
24 changed files with 728 additions and 363 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,13 @@ |
||||
import 'package:flutter/foundation.dart'; |
||||
|
||||
typedef CopyCutAction = Object? Function(dynamic data); |
||||
|
||||
/// An abstraction to make it easy to provide different implementations |
||||
/// For copy or cut actions from a Line (just for embeddable blocks) |
||||
@immutable |
||||
abstract class CopyCutService { |
||||
/// Get the CopyCutAction by the type |
||||
/// of the embeddable (this type is decided by |
||||
/// the property type of that class) |
||||
CopyCutAction getCopyCutAction(String type); |
||||
} |
@ -0,0 +1,19 @@ |
||||
import 'package:flutter/foundation.dart' show immutable; |
||||
import 'copy_cut_service.dart'; |
||||
import 'default_copy_cut_service.dart'; |
||||
|
||||
@immutable |
||||
class CopyCutServiceProvider { |
||||
const CopyCutServiceProvider._(); |
||||
static CopyCutService _instance = DefaultCopyCutService(); |
||||
|
||||
static CopyCutService get instance => _instance; |
||||
|
||||
static void setInstance(CopyCutService service) { |
||||
_instance = service; |
||||
} |
||||
|
||||
static void setInstanceToDefault() { |
||||
_instance = DefaultCopyCutService(); |
||||
} |
||||
} |
@ -0,0 +1,14 @@ |
||||
import '../../document/nodes/leaf.dart'; |
||||
import 'copy_cut_service.dart'; |
||||
|
||||
/// Default implementation for [CopyCutService] |
||||
/// |
||||
/// This implementation always return the default embed character |
||||
/// replacemenet ([\uFFFC]) to work with the embeds from the internal |
||||
/// flutter quill plugins |
||||
class DefaultCopyCutService extends CopyCutService { |
||||
@override |
||||
CopyCutAction getCopyCutAction(String type) { |
||||
return (data) => Embed.kObjectReplacementCharacter; |
||||
} |
||||
} |
@ -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)); |
||||
}); |
||||
}); |
||||
} |
Loading…
Reference in new issue