copy/cut select image and text together.

pull/1307/head
hehong 2 years ago
parent 954d5b9444
commit 41e79a407c
  1. 3
      CHANGELOG.md
  2. 7
      lib/src/models/documents/document.dart
  3. 56
      lib/src/models/documents/nodes/line.dart
  4. 10
      lib/src/widgets/controller.dart
  5. 4
      lib/src/widgets/editor.dart
  6. 1
      lib/src/widgets/raw_editor.dart
  7. 37
      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] # [7.2.11]
- Add affinity for localPosition. - Add affinity for localPosition.

@ -159,12 +159,7 @@ class Document {
return (res.node as Line).collectStyle(res.offset, len); return (res.node as Line).collectStyle(res.offset, len);
} }
/// Returns all styles for each node within selection /// Returns all styles and Embed for each node within selection
List<OffsetValue<Style>> collectAllIndividualStyles(int index, int len) {
final res = queryChild(index);
return (res.node as Line).collectAllIndividualStyles(res.offset, len);
}
List<OffsetValue> collectAllIndividualStyleAndEmbed(int index, int len) { List<OffsetValue> collectAllIndividualStyleAndEmbed(int index, int len) {
final res = queryChild(index); final res = queryChild(index);
return (res.node as Line) return (res.node as Line)

@ -395,41 +395,7 @@ class Line extends Container<Leaf?> {
} }
/// Returns each node segment's offset in selection /// Returns each node segment's offset in selection
/// with its corresponding style as a list /// with its corresponding style or embed as a list
List<OffsetValue<Style>> collectAllIndividualStyles(int offset, int len,
{int beg = 0}) {
final local = math.min(length - offset, len);
final result = <OffsetValue<Style>>[];
final data = queryChild(offset, true);
var node = data.node as Leaf?;
if (node != null) {
var pos = 0;
if (node is Text) {
pos = node.length - data.offset;
result.add(OffsetValue(beg, node.style));
}
while (!node!.isLast && pos < local) {
node = node.next as Leaf;
if (node is Text) {
result.add(OffsetValue(pos + beg, node.style));
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);
result.addAll(rest);
}
return result;
}
List<OffsetValue> collectAllIndividualStylesAndEmbed(int offset, int len, List<OffsetValue> collectAllIndividualStylesAndEmbed(int offset, int len,
{int beg = 0}) { {int beg = 0}) {
final local = math.min(length - offset, len); final local = math.min(length - offset, len);
@ -439,20 +405,16 @@ class Line extends Container<Leaf?> {
var node = data.node as Leaf?; var node = data.node as Leaf?;
if (node != null) { if (node != null) {
var pos = 0; var pos = 0;
if (node is Text) { if (node is Text || node.value is Embeddable) {
pos = node.length - data.offset; pos = node.length - data.offset;
result.add(OffsetValue(beg, node.style)); result.add(OffsetValue(
} else if (node.value is Embeddable) { beg, node is Text ? node.style : node.value as Embeddable));
pos = node.length - data.offset;
result.add(OffsetValue(beg, node.value as Embeddable));
} }
while (!node!.isLast && pos < local) { while (!node!.isLast && pos < local) {
node = node.next as Leaf; node = node.next as Leaf;
if (node is Text) { if (node is Text || node.value is Embeddable) {
result.add(OffsetValue(pos + beg, node.style)); result.add(OffsetValue(
pos += node.length; pos + beg, node is Text ? node.style : node.value as Embeddable));
} else if (node.value is Embeddable) {
result.add(OffsetValue(pos + beg, node.value as Embeddable));
pos += node.length; pos += node.length;
} }
} }
@ -460,8 +422,8 @@ class Line extends Container<Leaf?> {
final remaining = len - local; final remaining = len - local;
if (remaining > 0 && nextLine != null) { if (remaining > 0 && nextLine != null) {
final rest = final rest = nextLine!
nextLine!.collectAllIndividualStylesAndEmbed(0, remaining, beg: local); .collectAllIndividualStylesAndEmbed(0, remaining, beg: local);
result.addAll(rest); result.addAll(rest);
} }

@ -39,7 +39,9 @@ class QuillController extends ChangeNotifier {
/// Document managed by this controller. /// Document managed by this controller.
Document _document; Document _document;
Document get document => _document; Document get document => _document;
set document(doc) { set document(doc) {
_document = doc; _document = doc;
@ -159,13 +161,7 @@ class QuillController extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
/// Returns all styles for each node within selection /// Returns all styles and Embed for each node within selection
List<OffsetValue<Style>> getAllIndividualSelectionStyles() {
final styles = document.collectAllIndividualStyles(
selection.start, selection.end - selection.start);
return styles;
}
List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() { List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() {
final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed( final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed(
selection.start, selection.end - selection.start); selection.start, selection.end - selection.start);

@ -1,4 +1,5 @@
import 'dart:math' as math; import 'dart:math' as math;
// ignore: unnecessary_import // ignore: unnecessary_import
import 'dart:typed_data'; import 'dart:typed_data';
@ -13,7 +14,6 @@ import 'package:i18n_extension/i18n_widget.dart';
import '../models/documents/document.dart'; import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node; import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/leaf.dart'; import '../models/documents/nodes/leaf.dart';
import '../models/documents/style.dart';
import '../models/structs/offset_value.dart'; import '../models/structs/offset_value.dart';
import '../models/themes/quill_dialog_theme.dart'; import '../models/themes/quill_dialog_theme.dart';
import '../utils/platform.dart'; import '../utils/platform.dart';
@ -369,6 +369,7 @@ class QuillEditor extends StatefulWidget {
// Returns whether gesture is handled // Returns whether gesture is handled
final bool Function(LongPressMoveUpdateDetails details, final bool Function(LongPressMoveUpdateDetails details,
TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate; TextPosition Function(Offset offset))? onSingleLongTapMoveUpdate;
// Returns whether gesture is handled // Returns whether gesture is handled
final bool Function( final bool Function(
LongPressEndDetails details, TextPosition Function(Offset offset))? LongPressEndDetails details, TextPosition Function(Offset offset))?
@ -994,6 +995,7 @@ class RenderEditor extends RenderEditableContainerBox
} }
double? _maxContentWidth; double? _maxContentWidth;
set maxContentWidth(double? value) { set maxContentWidth(double? value) {
if (_maxContentWidth == value) return; if (_maxContentWidth == value) return;
_maxContentWidth = value; _maxContentWidth = value;

@ -20,7 +20,6 @@ import '../models/documents/nodes/embeddable.dart';
import '../models/documents/nodes/leaf.dart' as leaf; import '../models/documents/nodes/leaf.dart' as leaf;
import '../models/documents/nodes/line.dart'; import '../models/documents/nodes/line.dart';
import '../models/documents/nodes/node.dart'; import '../models/documents/nodes/node.dart';
import '../models/documents/style.dart';
import '../models/structs/offset_value.dart'; import '../models/structs/offset_value.dart';
import '../models/structs/vertical_spacing.dart'; import '../models/structs/vertical_spacing.dart';
import '../models/themes/quill_dialog_theme.dart'; import '../models/themes/quill_dialog_theme.dart';

@ -3,17 +3,14 @@ import 'dart:math' as math;
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../../../flutter_quill.dart';
import '../../models/documents/document.dart'; import '../../models/documents/document.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart'; import '../../models/documents/nodes/leaf.dart';
import '../../utils/delta.dart'; import '../../utils/delta.dart';
import '../editor.dart'; import '../editor.dart';
mixin RawEditorStateSelectionDelegateMixin on EditorState mixin RawEditorStateSelectionDelegateMixin on EditorState
implements TextSelectionDelegate { implements TextSelectionDelegate {
bool isContainEmbed = false;
@override @override
TextEditingValue get textEditingValue { TextEditingValue get textEditingValue {
return widget.controller.plainTextEditingValue; return widget.controller.plainTextEditingValue;
@ -30,17 +27,22 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
return; return;
} }
isContainEmbed = false; var insertedText = diff.inserted;
final insertedText = _adjustInsertedText(diff.inserted); final containsEmbed =
insertedText.codeUnits.contains(Embed.kObjectReplacementInt);
insertedText =
containsEmbed ? _adjustInsertedText(diff.inserted) : diff.inserted;
widget.controller.replaceText( widget.controller.replaceText(
diff.start, diff.deleted.length, insertedText, value.selection); diff.start, diff.deleted.length, insertedText, value.selection);
_applyPasteStyle(insertedText, diff.start); _applyPasteStyleAndEmbed(insertedText, diff.start, containsEmbed);
} }
void _applyPasteStyle(String insertedText, int start) { void _applyPasteStyleAndEmbed(
if (insertedText == pastePlainText && pastePlainText != '') { String insertedText, int start, bool containsEmbed) {
if (insertedText == pastePlainText && pastePlainText != '' ||
containsEmbed) {
final pos = start; final pos = start;
for (var i = 0; i < pasteStyleAndEmbed.length; i++) { for (var i = 0; i < pasteStyleAndEmbed.length; i++) {
final offset = pasteStyleAndEmbed[i].offset; final offset = pasteStyleAndEmbed[i].offset;
@ -57,30 +59,13 @@ mixin RawEditorStateSelectionDelegateMixin on EditorState
styleAndEmbed); styleAndEmbed);
} }
} }
}else if(isContainEmbed){
final pos = start;
for (var i = 0; i < pasteStyleAndEmbed.length; i++) {
final offset = pasteStyleAndEmbed[i].offset;
final style = pasteStyleAndEmbed[i].value;
if (style is Embeddable) {
widget.controller.replaceText(pos + offset, 0, style, null);
}
}
} }
} }
String _adjustInsertedText(String text) { 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(); final sb = StringBuffer();
for (var i = 0; i < text.length; i++) { for (var i = 0; i < text.length; i++) {
if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) { if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) {
isContainEmbed = true;
continue; continue;
} }
sb.write(text[i]); sb.write(text[i]);

@ -1,6 +1,6 @@
name: flutter_quill name: flutter_quill
description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us) description: A rich text editor supporting mobile and web (Demo App @ bulletjournal.us)
version: 7.2.11 version: 7.2.12
#author: bulletjournal #author: bulletjournal
homepage: https://bulletjournal.us/home/index.html homepage: https://bulletjournal.us/home/index.html
repository: https://github.com/singerdmx/flutter-quill repository: https://github.com/singerdmx/flutter-quill

@ -101,12 +101,17 @@ void main() {
expect(controller.getAllSelectionStyles(), everyElement(Style())); expect(controller.getAllSelectionStyles(), everyElement(Style()));
}); });
test('getAllIndividualSelectionStyles', () { test('getAllIndividualSelectionStylesAndEmbed', () {
controller.formatText(0, 2, Attribute.bold); controller
final result = controller.getAllIndividualSelectionStyles(); ..formatText(0, 2, Attribute.bold)
expect(result.length, 1); ..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].offset, 0);
expect(result[0].value, Style().put(Attribute.bold)); expect(result[0].value, Style().put(Attribute.bold));
expect((result[1].value as Embeddable).type, BlockEmbed.imageType);
}); });
test('getPlainText', () { test('getPlainText', () {

Loading…
Cancel
Save