copy/cut select image and text together. (#1307)

* copy/cut select image and text together.

* copy/cut select image and text together.
pull/1309/head
He Hong 2 years ago committed by GitHub
parent ffbb17c776
commit a353ef1a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      CHANGELOG.md
  2. 7
      lib/src/models/documents/document.dart
  3. 22
      lib/src/models/documents/nodes/line.dart
  4. 10
      lib/src/widgets/controller.dart
  5. 6
      lib/src/widgets/editor.dart
  6. 9
      lib/src/widgets/raw_editor.dart
  7. 45
      lib/src/widgets/raw_editor/raw_editor_state_selection_delegate_mixin.dart
  8. 2
      pubspec.yaml
  9. 13
      test/widgets/controller_test.dart

@ -1,3 +1,6 @@
# [7.2.12]
- Add support for copy/cut select image and text together.
# [7.2.11]
- Add affinity for localPosition.

@ -159,10 +159,11 @@ class Document {
return (res.node as Line).collectStyle(res.offset, len);
}
/// Returns all styles for each node within selection
List<OffsetValue<Style>> collectAllIndividualStyles(int index, int len) {
/// Returns all styles and Embed for each node within selection
List<OffsetValue> 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.

@ -395,35 +395,35 @@ class Line extends Container<Leaf?> {
}
/// Returns each node segment's offset in selection
/// with its corresponding style as a list
List<OffsetValue<Style>> collectAllIndividualStyles(int offset, int len,
/// with its corresponding style or embed as a list
List<OffsetValue> collectAllIndividualStylesAndEmbed(int offset, int len,
{int beg = 0}) {
final local = math.min(length - offset, len);
final result = <OffsetValue<Style>>[];
final result = <OffsetValue>[];
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);
}

@ -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<OffsetValue<Style>> getAllIndividualSelectionStyles() {
final styles = document.collectAllIndividualStyles(
/// Returns all styles and Embed for each node within selection
List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() {
final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed(
selection.start, selection.end - selection.start);
return styles;
return stylesAndEmbed;
}
/// Returns plain text for each node within selection

@ -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<RawEditor>
EditorTextSelectionOverlay? get selectionOverlay;
List<OffsetValue<Style>> get pasteStyle;
List<OffsetValue> 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;

@ -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<OffsetValue<Style>> get pasteStyle => _pasteStyle;
List<OffsetValue<Style>> _pasteStyle = <OffsetValue<Style>>[];
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed;
List<OffsetValue> _pasteStyleAndEmbed = <OffsetValue>[];
@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;

@ -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) {

@ -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

@ -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', () {

Loading…
Cancel
Save