diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d597df9..0907936e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# [7.2.12] +- Add support for copy/cut select image and text together. + # [7.2.11] - Add affinity for localPosition. diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 122c9a9c..5f3b82fc 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -159,10 +159,11 @@ class Document { return (res.node as Line).collectStyle(res.offset, len); } - /// Returns all styles for each node within selection - List> collectAllIndividualStyles(int index, int len) { + /// Returns all styles and Embed for each node within selection + List collectAllIndividualStyleAndEmbed(int index, int len) { final res = queryChild(index); - return (res.node as Line).collectAllIndividualStyles(res.offset, len); + return (res.node as Line) + .collectAllIndividualStylesAndEmbed(res.offset, len); } /// Returns all styles for any character within the specified text range. diff --git a/lib/src/models/documents/nodes/line.dart b/lib/src/models/documents/nodes/line.dart index fdd342ac..bb06bbf9 100644 --- a/lib/src/models/documents/nodes/line.dart +++ b/lib/src/models/documents/nodes/line.dart @@ -395,35 +395,35 @@ class Line extends Container { } /// Returns each node segment's offset in selection - /// with its corresponding style as a list - List> collectAllIndividualStyles(int offset, int len, + /// with its corresponding style or embed as a list + List collectAllIndividualStylesAndEmbed(int offset, int len, {int beg = 0}) { final local = math.min(length - offset, len); - final result = >[]; + final result = []; final data = queryChild(offset, true); var node = data.node as Leaf?; if (node != null) { var pos = 0; - if (node is Text) { + if (node is Text || node.value is Embeddable) { pos = node.length - data.offset; - result.add(OffsetValue(beg, node.style)); + result.add(OffsetValue( + beg, node is Text ? node.style : node.value as Embeddable)); } while (!node!.isLast && pos < local) { node = node.next as Leaf; - if (node is Text) { - result.add(OffsetValue(pos + beg, node.style)); + if (node is Text || node.value is Embeddable) { + result.add(OffsetValue( + pos + beg, node is Text ? node.style : node.value as Embeddable)); pos += node.length; } } } - // TODO: add line style and parent's block style - final remaining = len - local; if (remaining > 0 && nextLine != null) { - final rest = - nextLine!.collectAllIndividualStyles(0, remaining, beg: local); + final rest = nextLine! + .collectAllIndividualStylesAndEmbed(0, remaining, beg: local); result.addAll(rest); } diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index aedd7015..a3732449 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -39,7 +39,9 @@ class QuillController extends ChangeNotifier { /// Document managed by this controller. Document _document; + Document get document => _document; + set document(doc) { _document = doc; @@ -159,11 +161,11 @@ class QuillController extends ChangeNotifier { notifyListeners(); } - /// Returns all styles for each node within selection - List> getAllIndividualSelectionStyles() { - final styles = document.collectAllIndividualStyles( + /// Returns all styles and Embed for each node within selection + List getAllIndividualSelectionStylesAndEmbed() { + final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed( selection.start, selection.end - selection.start); - return styles; + return stylesAndEmbed; } /// Returns plain text for each node within selection diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index 554c1239..03db3ac6 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; + // ignore: unnecessary_import import 'dart:typed_data'; @@ -13,7 +14,6 @@ import 'package:i18n_extension/i18n_widget.dart'; import '../models/documents/document.dart'; import '../models/documents/nodes/container.dart' as container_node; import '../models/documents/nodes/leaf.dart'; -import '../models/documents/style.dart'; import '../models/structs/offset_value.dart'; import '../models/themes/quill_dialog_theme.dart'; import '../utils/platform.dart'; @@ -38,7 +38,7 @@ abstract class EditorState extends State EditorTextSelectionOverlay? get selectionOverlay; - List> get pasteStyle; + List get pasteStyleAndEmbed; String get pastePlainText; @@ -369,6 +369,7 @@ class QuillEditor extends StatefulWidget { // Returns whether gesture is handled final bool Function(LongPressMoveUpdateDetails details, TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; + // Returns whether gesture is handled final bool Function( LongPressEndDetails details, TextPosition Function(Offset offset))? @@ -994,6 +995,7 @@ class RenderEditor extends RenderEditableContainerBox } double? _maxContentWidth; + set maxContentWidth(double? value) { if (_maxContentWidth == value) return; _maxContentWidth = value; diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index 48b65df0..1753c87d 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -20,7 +20,6 @@ import '../models/documents/nodes/embeddable.dart'; import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/node.dart'; -import '../models/documents/style.dart'; import '../models/structs/offset_value.dart'; import '../models/structs/vertical_spacing.dart'; import '../models/themes/quill_dialog_theme.dart'; @@ -318,8 +317,8 @@ class RawEditorState extends EditorState // for pasting style @override - List> get pasteStyle => _pasteStyle; - List> _pasteStyle = >[]; + List get pasteStyleAndEmbed => _pasteStyleAndEmbed; + List _pasteStyleAndEmbed = []; @override String get pastePlainText => _pastePlainText; @@ -1435,7 +1434,7 @@ class RawEditorState extends EditorState void copySelection(SelectionChangedCause cause) { controller.copiedImageUrl = null; _pastePlainText = controller.getPlainText(); - _pasteStyle = controller.getAllIndividualSelectionStyles(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); final selection = textEditingValue.selection; final text = textEditingValue.text; @@ -1464,7 +1463,7 @@ class RawEditorState extends EditorState void cutSelection(SelectionChangedCause cause) { controller.copiedImageUrl = null; _pastePlainText = controller.getPlainText(); - _pasteStyle = controller.getAllIndividualSelectionStyles(); + _pasteStyleAndEmbed = controller.getAllIndividualSelectionStylesAndEmbed(); if (widget.readOnly) { return; diff --git a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart index fdd9244b..ac7c274d 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart @@ -4,6 +4,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import '../../models/documents/document.dart'; +import '../../models/documents/nodes/embeddable.dart'; import '../../models/documents/nodes/leaf.dart'; import '../../utils/delta.dart'; import '../editor.dart'; @@ -26,38 +27,42 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState return; } - final insertedText = _adjustInsertedText(diff.inserted); + var insertedText = diff.inserted; + final containsEmbed = + insertedText.codeUnits.contains(Embed.kObjectReplacementInt); + insertedText = + containsEmbed ? _adjustInsertedText(diff.inserted) : diff.inserted; widget.controller.replaceText( diff.start, diff.deleted.length, insertedText, value.selection); - _applyPasteStyle(insertedText, diff.start); + _applyPasteStyleAndEmbed(insertedText, diff.start, containsEmbed); } - void _applyPasteStyle(String insertedText, int start) { - if (insertedText == pastePlainText && pastePlainText != '') { + void _applyPasteStyleAndEmbed( + String insertedText, int start, bool containsEmbed) { + if (insertedText == pastePlainText && pastePlainText != '' || + containsEmbed) { final pos = start; - for (var i = 0; i < pasteStyle.length; i++) { - final offset = pasteStyle[i].offset; - final style = pasteStyle[i].value; - widget.controller.formatTextStyle( - pos + offset, - i == pasteStyle.length - 1 - ? pastePlainText.length - offset - : pasteStyle[i + 1].offset, - style); + for (var i = 0; i < pasteStyleAndEmbed.length; i++) { + final offset = pasteStyleAndEmbed[i].offset; + final styleAndEmbed = pasteStyleAndEmbed[i].value; + + if (styleAndEmbed is Embeddable) { + widget.controller.replaceText(pos + offset, 0, styleAndEmbed, null); + } else { + widget.controller.formatTextStyle( + pos + offset, + i == pasteStyleAndEmbed.length - 1 + ? pastePlainText.length - offset + : pasteStyleAndEmbed[i + 1].offset, + styleAndEmbed); + } } } } String _adjustInsertedText(String text) { - // For clip from editor, it may contain image, a.k.a 65532 or '\uFFFC'. - // For clip from browser, image is directly ignore. - // Here we skip image when pasting. - if (!text.codeUnits.contains(Embed.kObjectReplacementInt)) { - return text; - } - final sb = StringBuffer(); for (var i = 0; i < text.length; i++) { if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) { diff --git a/pubspec.yaml b/pubspec.yaml index f2337b51..a9442559 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_quill description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) -version: 7.2.11 +version: 7.2.12 #author: bulletjournal homepage: https://bulletjournal.us/home/index.html repository: https://github.com/singerdmx/flutter-quill diff --git a/test/widgets/controller_test.dart b/test/widgets/controller_test.dart index 047dfcae..415c51f6 100644 --- a/test/widgets/controller_test.dart +++ b/test/widgets/controller_test.dart @@ -101,12 +101,17 @@ void main() { expect(controller.getAllSelectionStyles(), everyElement(Style())); }); - test('getAllIndividualSelectionStyles', () { - controller.formatText(0, 2, Attribute.bold); - final result = controller.getAllIndividualSelectionStyles(); - expect(result.length, 1); + test('getAllIndividualSelectionStylesAndEmbed', () { + controller + ..formatText(0, 2, Attribute.bold) + ..replaceText(2, 2, BlockEmbed.image('/test'),null) + ..updateSelection(const TextSelection(baseOffset: 0, extentOffset: 4), + ChangeSource.REMOTE); + final result = controller.getAllIndividualSelectionStylesAndEmbed(); + expect(result.length, 2); expect(result[0].offset, 0); expect(result[0].value, Style().put(Attribute.bold)); + expect((result[1].value as Embeddable).type, BlockEmbed.imageType); }); test('getPlainText', () {