Skip to content

Commit 6f8fa24

Browse files
Fix Multiline paste with attributes and embeds (singerdmx#2074)
1 parent 1c3ccf7 commit 6f8fa24

File tree

8 files changed

+285
-86
lines changed

8 files changed

+285
-86
lines changed

lib/src/controller/quill_controller.dart

Lines changed: 52 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import '../document/nodes/embeddable.dart';
1717
import '../document/nodes/leaf.dart';
1818
import '../document/structs/doc_change.dart';
1919
import '../document/style.dart';
20+
import '../editor/config/editor_configurations.dart';
2021
import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
2122
import 'quill_controller_configurations.dart';
2223

@@ -39,19 +40,25 @@ class QuillController extends ChangeNotifier {
3940
_selection = selection;
4041

4142
factory QuillController.basic(
42-
{QuillControllerConfigurations configurations =
43-
const QuillControllerConfigurations(),
44-
FocusNode? editorFocusNode}) {
45-
return QuillController(
46-
configurations: configurations,
47-
editorFocusNode: editorFocusNode,
48-
document: Document(),
49-
selection: const TextSelection.collapsed(offset: 0),
50-
);
51-
}
43+
{QuillControllerConfigurations configurations =
44+
const QuillControllerConfigurations(),
45+
FocusNode? editorFocusNode}) =>
46+
QuillController(
47+
configurations: configurations,
48+
editorFocusNode: editorFocusNode,
49+
document: Document(),
50+
selection: const TextSelection.collapsed(offset: 0),
51+
);
5252

5353
final QuillControllerConfigurations configurations;
5454

55+
/// Local copy of editor configurations enables fail-safe setting from editor _initState method
56+
QuillEditorConfigurations? _editorConfigurations;
57+
QuillEditorConfigurations? get editorConfigurations =>
58+
configurations.editorConfigurations ?? _editorConfigurations;
59+
set editorConfigurations(QuillEditorConfigurations? value) =>
60+
_editorConfigurations = value;
61+
5562
/// Document managed by this controller.
5663
Document _document;
5764

@@ -476,10 +483,13 @@ class QuillController extends ChangeNotifier {
476483

477484
/// Clipboard caches last copy to allow paste with styles. Static to allow paste between multiple instances of editor.
478485
static String _pastePlainText = '';
486+
static Delta _pasteDelta = Delta();
479487
static List<OffsetValue> _pasteStyleAndEmbed = <OffsetValue>[];
480488

481489
String get pastePlainText => _pastePlainText;
490+
Delta get pasteDelta => _pasteDelta;
482491
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed;
492+
483493
bool readOnly;
484494

485495
/// Used to give focus to the editor following a toolbar action
@@ -495,9 +505,17 @@ class QuillController extends ChangeNotifier {
495505

496506
bool clipboardSelection(bool copy) {
497507
copiedImageUrl = null;
498-
_pastePlainText = getPlainText();
508+
509+
/// Get the text for the selected region and expand the content of Embedded objects.
510+
_pastePlainText = document.getPlainText(
511+
selection.start, selection.end - selection.start, editorConfigurations);
512+
513+
/// Get the internal representation so it can be pasted into a QuillEditor with style retained.
499514
_pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed();
500515

516+
/// Get the deltas for the selection so they can be pasted into a QuillEditor with styles and embeds retained.
517+
_pasteDelta = document.toDelta().slice(selection.start, selection.end);
518+
501519
if (!selection.isCollapsed) {
502520
Clipboard.setData(ClipboardData(text: _pastePlainText));
503521
if (!copy) {
@@ -538,28 +556,7 @@ class QuillController extends ChangeNotifier {
538556
// See https://github.com/flutter/flutter/issues/11427
539557
final plainTextClipboardData =
540558
await Clipboard.getData(Clipboard.kTextPlain);
541-
if (plainTextClipboardData != null) {
542-
final lines = plainTextClipboardData.text!.split('\n');
543-
for (var i = 0; i < lines.length; ++i) {
544-
final line = lines[i];
545-
if (line.isNotEmpty) {
546-
replaceTextWithEmbeds(
547-
selection.start,
548-
selection.end - selection.start,
549-
line,
550-
TextSelection.collapsed(offset: selection.start + line.length),
551-
);
552-
}
553-
if (i != lines.length - 1) {
554-
document.insert(selection.extentOffset, '\n');
555-
_updateSelection(
556-
TextSelection.collapsed(
557-
offset: selection.extentOffset + 1,
558-
),
559-
insertNewline: true,
560-
);
561-
}
562-
}
559+
if (pasteUsingPlainOrDelta(plainTextClipboardData?.text)) {
563560
updateEditor?.call();
564561
return true;
565562
}
@@ -572,6 +569,28 @@ class QuillController extends ChangeNotifier {
572569
return false;
573570
}
574571

572+
/// Internal method to allow unit testing
573+
bool pasteUsingPlainOrDelta(String? clipboardText) {
574+
if (clipboardText != null) {
575+
/// Internal copy-paste preserves styles and embeds
576+
if (clipboardText == _pastePlainText &&
577+
_pastePlainText.isNotEmpty &&
578+
_pasteDelta.isNotEmpty) {
579+
replaceText(selection.start, selection.end - selection.start,
580+
_pasteDelta, TextSelection.collapsed(offset: selection.end));
581+
} else {
582+
replaceText(
583+
selection.start,
584+
selection.end - selection.start,
585+
clipboardText,
586+
TextSelection.collapsed(
587+
offset: selection.end + clipboardText.length));
588+
}
589+
return true;
590+
}
591+
return false;
592+
}
593+
575594
void _pasteUsingDelta(Delta deltaFromClipboard) {
576595
replaceText(
577596
selection.start,

lib/src/controller/quill_controller_configurations.dart

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
import '../editor/config/editor_configurations.dart';
2+
13
class QuillControllerConfigurations {
24
const QuillControllerConfigurations(
3-
{this.onClipboardPaste, this.requireScriptFontFeatures = false});
5+
{this.editorConfigurations,
6+
this.onClipboardPaste,
7+
this.requireScriptFontFeatures = false});
8+
9+
/// Provides central access to editor configurations required for controller actions
10+
///
11+
/// Future: will be changed to 'required final'
12+
final QuillEditorConfigurations? editorConfigurations;
413

514
/// Callback when the user pastes and data has not already been processed
615
///

lib/src/delta/delta_diff.dart

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,18 @@ int getPositionDelta(Delta user, Delta actual) {
7070
);
7171
}
7272
if (userOperation.key == actualOperation.key) {
73+
/// Insertions must update diff allowing for type mismatch of Operation
74+
if (userOperation.key == Operation.insertKey) {
75+
if (userOperation.data is Delta && actualOperation.data is String) {
76+
diff += actualOperation.length!;
77+
}
78+
}
7379
continue;
7480
} else if (userOperation.isInsert && actualOperation.isRetain) {
7581
diff -= userOperation.length!;
7682
} else if (userOperation.isDelete && actualOperation.isRetain) {
7783
diff += userOperation.length!;
7884
} else if (userOperation.isRetain && actualOperation.isInsert) {
79-
String? operationTxt = '';
80-
if (actualOperation.data is String) {
81-
operationTxt = actualOperation.data as String?;
82-
}
83-
if (operationTxt!.startsWith('\n')) {
84-
continue;
85-
}
8685
diff += actualOperation.length!;
8786
}
8887
}

lib/src/document/document.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import '../../quill_delta.dart';
66
import '../common/structs/offset_value.dart';
77
import '../common/structs/segment_leaf_node.dart';
88
import '../delta/delta_x.dart';
9+
import '../editor/config/editor_configurations.dart';
910
import '../editor/embed/embed_editor_builder.dart';
1011
import '../rules/rule.dart';
1112
import 'attribute.dart';
@@ -239,9 +240,9 @@ class Document {
239240
}
240241

241242
/// Returns plain text within the specified text range.
242-
String getPlainText(int index, int len) {
243+
String getPlainText(int index, int len, [QuillEditorConfigurations? config]) {
243244
final res = queryChild(index);
244-
return (res.node as Line).getPlainText(res.offset, len);
245+
return (res.node as Line).getPlainText(res.offset, len, config);
245246
}
246247

247248
/// Returns [Line] located at specified character [offset].

lib/src/document/nodes/line.dart

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:collection/collection.dart';
44

55
import '../../../../quill_delta.dart';
66
import '../../common/structs/offset_value.dart';
7+
import '../../editor/config/editor_configurations.dart';
78
import '../../editor/embed/embed_editor_builder.dart';
89
import '../../editor_toolbar_controller_shared/copy_cut_service/copy_cut_service_provider.dart';
910
import '../attribute.dart';
@@ -512,14 +513,17 @@ base class Line extends QuillContainer<Leaf?> {
512513
}
513514

514515
/// Returns plain text within the specified text range.
515-
String getPlainText(int offset, int len) {
516+
String getPlainText(int offset, int len,
517+
[QuillEditorConfigurations? config]) {
516518
final plainText = StringBuffer();
517-
_getPlainText(offset, len, plainText);
519+
_getPlainText(offset, len, plainText, config);
518520
return plainText.toString();
519521
}
520522

521-
int _getNodeText(Leaf node, StringBuffer buffer, int offset, int remaining) {
522-
final text = node.toPlainText();
523+
int _getNodeText(Leaf node, StringBuffer buffer, int offset, int remaining,
524+
QuillEditorConfigurations? config) {
525+
final text =
526+
node.toPlainText(config?.embedBuilders, config?.unknownEmbedBuilder);
523527
if (text == Embed.kObjectReplacementCharacter) {
524528
final embed = node.value as Embeddable;
525529
final provider = CopyCutServiceProvider.instance;
@@ -539,12 +543,19 @@ base class Line extends QuillContainer<Leaf?> {
539543
return remaining - node.length;
540544
}
541545

546+
/// Text for clipboard will expand the content of Embed nodes
547+
if (node is Embed && config != null) {
548+
buffer.write(text);
549+
return remaining - 1;
550+
}
551+
542552
final end = math.min(offset + remaining, text.length);
543553
buffer.write(text.substring(offset, end));
544554
return remaining - (end - offset);
545555
}
546556

547-
int _getPlainText(int offset, int len, StringBuffer plainText) {
557+
int _getPlainText(int offset, int len, StringBuffer plainText,
558+
QuillEditorConfigurations? config) {
548559
var len0 = len;
549560
final data = queryChild(offset, false);
550561
var node = data.node as Leaf?;
@@ -555,11 +566,12 @@ base class Line extends QuillContainer<Leaf?> {
555566
plainText.write('\n');
556567
len0 -= 1;
557568
} else {
558-
len0 = _getNodeText(node, plainText, offset - node.offset, len0);
569+
len0 =
570+
_getNodeText(node, plainText, offset - node.offset, len0, config);
559571

560572
while (!node!.isLast && len0 > 0) {
561573
node = node.next as Leaf;
562-
len0 = _getNodeText(node, plainText, 0, len0);
574+
len0 = _getNodeText(node, plainText, 0, len0, config);
563575
}
564576

565577
if (len0 > 0) {
@@ -570,7 +582,7 @@ base class Line extends QuillContainer<Leaf?> {
570582
}
571583

572584
if (len0 > 0 && nextLine != null) {
573-
len0 = nextLine!._getPlainText(0, len0, plainText);
585+
len0 = nextLine!._getPlainText(0, len0, plainText, config);
574586
}
575587
}
576588

lib/src/editor/editor.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,17 +174,18 @@ class QuillEditorState extends State<QuillEditor>
174174
@override
175175
void initState() {
176176
super.initState();
177-
178177
_editorKey = configurations.editorKey ?? GlobalKey<EditorState>();
179178
_selectionGestureDetectorBuilder =
180179
_QuillEditorSelectionGestureDetectorBuilder(
181180
this,
182181
configurations.detectWordBoundary,
183182
);
184183

184+
widget.configurations.controller.editorConfigurations ??=
185+
widget.configurations;
186+
185187
final focusNode =
186-
widget.configurations.controller.editorFocusNode ?? widget.focusNode;
187-
widget.configurations.controller.editorFocusNode = focusNode;
188+
widget.configurations.controller.editorFocusNode ??= widget.focusNode;
188189

189190
if (configurations.autoFocus) {
190191
focusNode.requestFocus();

0 commit comments

Comments
 (0)