mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #807 from AppFlowy-IO/refactor/text-delta
Fix: emoji position and unicode issues
This commit is contained in:
commit
7ac5f822c9
@ -219,7 +219,5 @@ class TextNode extends Node {
|
|||||||
delta: delta ?? this.delta,
|
delta: delta ?? this.delta,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: It's unneccesry to compute everytime.
|
String toRawString() => _delta.toRawString();
|
||||||
String toRawString() =>
|
|
||||||
_delta.operations.whereType<TextInsert>().map((op) => op.content).join();
|
|
||||||
}
|
}
|
||||||
|
@ -257,8 +257,10 @@ TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// basically copy from: https://github.com/quilljs/delta
|
// basically copy from: https://github.com/quilljs/delta
|
||||||
class Delta {
|
class Delta extends Iterable<TextOperation> {
|
||||||
final List<TextOperation> operations;
|
final List<TextOperation> _operations;
|
||||||
|
String? _rawString;
|
||||||
|
List<int>? _runeIndexes;
|
||||||
|
|
||||||
factory Delta.fromJson(List<dynamic> list) {
|
factory Delta.fromJson(List<dynamic> list) {
|
||||||
final operations = <TextOperation>[];
|
final operations = <TextOperation>[];
|
||||||
@ -273,51 +275,50 @@ class Delta {
|
|||||||
return Delta(operations);
|
return Delta(operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[];
|
Delta([List<TextOperation>? ops]) : _operations = ops ?? <TextOperation>[];
|
||||||
|
|
||||||
Delta addAll(List<TextOperation> textOps) {
|
void addAll(Iterable<TextOperation> textOps) {
|
||||||
textOps.forEach(add);
|
textOps.forEach(add);
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta add(TextOperation textOp) {
|
void add(TextOperation textOp) {
|
||||||
if (textOp.isEmpty) {
|
if (textOp.isEmpty) {
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
|
_rawString = null;
|
||||||
|
|
||||||
if (operations.isNotEmpty) {
|
if (_operations.isNotEmpty) {
|
||||||
final lastOp = operations.last;
|
final lastOp = _operations.last;
|
||||||
if (lastOp is TextDelete && textOp is TextDelete) {
|
if (lastOp is TextDelete && textOp is TextDelete) {
|
||||||
lastOp.length += textOp.length;
|
lastOp.length += textOp.length;
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
if (mapEquals(lastOp.attributes, textOp.attributes)) {
|
if (mapEquals(lastOp.attributes, textOp.attributes)) {
|
||||||
if (lastOp is TextInsert && textOp is TextInsert) {
|
if (lastOp is TextInsert && textOp is TextInsert) {
|
||||||
lastOp.content += textOp.content;
|
lastOp.content += textOp.content;
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
// if there is an delete before the insert
|
// if there is an delete before the insert
|
||||||
// swap the order
|
// swap the order
|
||||||
if (lastOp is TextDelete && textOp is TextInsert) {
|
if (lastOp is TextDelete && textOp is TextInsert) {
|
||||||
operations.removeLast();
|
_operations.removeLast();
|
||||||
operations.add(textOp);
|
_operations.add(textOp);
|
||||||
operations.add(lastOp);
|
_operations.add(lastOp);
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
if (lastOp is TextRetain && textOp is TextRetain) {
|
if (lastOp is TextRetain && textOp is TextRetain) {
|
||||||
lastOp.length += textOp.length;
|
lastOp.length += textOp.length;
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
operations.add(textOp);
|
_operations.add(textOp);
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta slice(int start, [int? end]) {
|
Delta slice(int start, [int? end]) {
|
||||||
final result = Delta();
|
final result = Delta();
|
||||||
final iterator = _OpIterator(operations);
|
final iterator = _OpIterator(_operations);
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
|
||||||
while ((end == null || index < end) && iterator.hasNext) {
|
while ((end == null || index < end) && iterator.hasNext) {
|
||||||
@ -335,29 +336,22 @@ class Delta {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta insert(String content, [Attributes? attributes]) {
|
void insert(String content, [Attributes? attributes]) =>
|
||||||
final op = TextInsert(content, attributes);
|
add(TextInsert(content, attributes));
|
||||||
return add(op);
|
|
||||||
}
|
|
||||||
|
|
||||||
Delta retain(int length, [Attributes? attributes]) {
|
void retain(int length, [Attributes? attributes]) =>
|
||||||
final op = TextRetain(length, attributes);
|
add(TextRetain(length, attributes));
|
||||||
return add(op);
|
|
||||||
}
|
|
||||||
|
|
||||||
Delta delete(int length) {
|
void delete(int length) => add(TextDelete(length));
|
||||||
final op = TextDelete(length);
|
|
||||||
return add(op);
|
|
||||||
}
|
|
||||||
|
|
||||||
int get length {
|
int get length {
|
||||||
return operations.fold(
|
return _operations.fold(
|
||||||
0, (previousValue, element) => previousValue + element.length);
|
0, (previousValue, element) => previousValue + element.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta compose(Delta other) {
|
Delta compose(Delta other) {
|
||||||
final thisIter = _OpIterator(operations);
|
final thisIter = _OpIterator(_operations);
|
||||||
final otherIter = _OpIterator(other.operations);
|
final otherIter = _OpIterator(other._operations);
|
||||||
final ops = <TextOperation>[];
|
final ops = <TextOperation>[];
|
||||||
|
|
||||||
final firstOther = otherIter.peek();
|
final firstOther = otherIter.peek();
|
||||||
@ -405,9 +399,9 @@ class Delta {
|
|||||||
|
|
||||||
// Optimization if rest of other is just retain
|
// Optimization if rest of other is just retain
|
||||||
if (!otherIter.hasNext &&
|
if (!otherIter.hasNext &&
|
||||||
delta.operations[delta.operations.length - 1] == newOp) {
|
delta._operations[delta._operations.length - 1] == newOp) {
|
||||||
final rest = Delta(thisIter.rest());
|
final rest = Delta(thisIter.rest());
|
||||||
return delta.concat(rest).chop();
|
return (delta + rest)..chop();
|
||||||
}
|
}
|
||||||
} else if (otherOp is TextDelete && (thisOp is TextRetain)) {
|
} else if (otherOp is TextDelete && (thisOp is TextRetain)) {
|
||||||
delta.add(otherOp);
|
delta.add(otherOp);
|
||||||
@ -415,27 +409,27 @@ class Delta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return delta.chop();
|
return delta..chop();
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta concat(Delta other) {
|
Delta operator +(Delta other) {
|
||||||
var ops = [...operations];
|
var ops = [..._operations];
|
||||||
if (other.operations.isNotEmpty) {
|
if (other._operations.isNotEmpty) {
|
||||||
ops.add(other.operations[0]);
|
ops.add(other._operations[0]);
|
||||||
ops.addAll(other.operations.sublist(1));
|
ops.addAll(other._operations.sublist(1));
|
||||||
}
|
}
|
||||||
return Delta(ops);
|
return Delta(ops);
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta chop() {
|
void chop() {
|
||||||
if (operations.isEmpty) {
|
if (_operations.isEmpty) {
|
||||||
return this;
|
return;
|
||||||
}
|
}
|
||||||
final lastOp = operations.last;
|
_rawString = null;
|
||||||
|
final lastOp = _operations.last;
|
||||||
if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) {
|
if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) {
|
||||||
operations.removeLast();
|
_operations.removeLast();
|
||||||
}
|
}
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -443,17 +437,17 @@ class Delta {
|
|||||||
if (other is! Delta) {
|
if (other is! Delta) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return listEquals(operations, other.operations);
|
return listEquals(_operations, other._operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
return hashList(operations);
|
return hashList(_operations);
|
||||||
}
|
}
|
||||||
|
|
||||||
Delta invert(Delta base) {
|
Delta invert(Delta base) {
|
||||||
final inverted = Delta();
|
final inverted = Delta();
|
||||||
operations.fold(0, (int previousValue, op) {
|
_operations.fold(0, (int previousValue, op) {
|
||||||
if (op is TextInsert) {
|
if (op is TextInsert) {
|
||||||
inverted.delete(op.length);
|
inverted.delete(op.length);
|
||||||
} else if (op is TextRetain && op.attributes == null) {
|
} else if (op is TextRetain && op.attributes == null) {
|
||||||
@ -462,7 +456,7 @@ class Delta {
|
|||||||
} else if (op is TextDelete || op is TextRetain) {
|
} else if (op is TextDelete || op is TextRetain) {
|
||||||
final length = op.length;
|
final length = op.length;
|
||||||
final slice = base.slice(previousValue, previousValue + length);
|
final slice = base.slice(previousValue, previousValue + length);
|
||||||
for (final baseOp in slice.operations) {
|
for (final baseOp in slice._operations) {
|
||||||
if (op is TextDelete) {
|
if (op is TextDelete) {
|
||||||
inverted.add(baseOp);
|
inverted.add(baseOp);
|
||||||
} else if (op is TextRetain && op.attributes != null) {
|
} else if (op is TextRetain && op.attributes != null) {
|
||||||
@ -474,10 +468,58 @@ class Delta {
|
|||||||
}
|
}
|
||||||
return previousValue;
|
return previousValue;
|
||||||
});
|
});
|
||||||
return inverted.chop();
|
return inverted..chop();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<dynamic> toJson() {
|
List<dynamic> toJson() {
|
||||||
return operations.map((e) => e.toJson()).toList();
|
return _operations.map((e) => e.toJson()).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int prevRunePosition(int pos) {
|
||||||
|
if (pos == 0) {
|
||||||
|
return pos - 1;
|
||||||
|
}
|
||||||
|
_rawString ??=
|
||||||
|
_operations.whereType<TextInsert>().map((op) => op.content).join();
|
||||||
|
_runeIndexes ??= stringIndexes(_rawString!);
|
||||||
|
return _runeIndexes![pos - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
int nextRunePosition(int pos) {
|
||||||
|
final stringContent = toRawString();
|
||||||
|
if (pos >= stringContent.length - 1) {
|
||||||
|
return stringContent.length;
|
||||||
|
}
|
||||||
|
_runeIndexes ??= stringIndexes(_rawString!);
|
||||||
|
|
||||||
|
for (var i = pos + 1; i < _runeIndexes!.length; i++) {
|
||||||
|
if (_runeIndexes![i] != pos) {
|
||||||
|
return _runeIndexes![i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringContent.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
String toRawString() {
|
||||||
|
_rawString ??=
|
||||||
|
_operations.whereType<TextInsert>().map((op) => op.content).join();
|
||||||
|
return _rawString!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterator<TextOperation> get iterator => _operations.iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> stringIndexes(String content) {
|
||||||
|
final indexes = List<int>.filled(content.length, 0);
|
||||||
|
final iterator = content.runes.iterator;
|
||||||
|
|
||||||
|
while (iterator.moveNext()) {
|
||||||
|
for (var i = 0; i < iterator.currentSize; i++) {
|
||||||
|
indexes[iterator.rawIndex + i] = iterator.rawIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexes;
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ extension TextNodeExtension on TextNode {
|
|||||||
allSatisfyInSelection(StyleKey.strikethrough, selection);
|
allSatisfyInSelection(StyleKey.strikethrough, selection);
|
||||||
|
|
||||||
bool allSatisfyInSelection(String styleKey, Selection selection) {
|
bool allSatisfyInSelection(String styleKey, Selection selection) {
|
||||||
final ops = delta.operations.whereType<TextInsert>();
|
final ops = delta.whereType<TextInsert>();
|
||||||
var start = 0;
|
var start = 0;
|
||||||
for (final op in ops) {
|
for (final op in ops) {
|
||||||
if (start >= selection.end.offset) {
|
if (start >= selection.end.offset) {
|
||||||
|
@ -71,7 +71,7 @@ class HTMLToNodesConverter {
|
|||||||
delta.insert(child.text ?? "");
|
delta.insert(child.text ?? "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (delta.operations.isNotEmpty) {
|
if (delta.isNotEmpty) {
|
||||||
result.add(TextNode(type: "text", delta: delta));
|
result.add(TextNode(type: "text", delta: delta));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -101,7 +101,7 @@ class HTMLToNodesConverter {
|
|||||||
} else {
|
} else {
|
||||||
final delta = Delta();
|
final delta = Delta();
|
||||||
delta.insert(element.text);
|
delta.insert(element.text);
|
||||||
if (delta.operations.isNotEmpty) {
|
if (delta.isNotEmpty) {
|
||||||
return [TextNode(type: "text", delta: delta)];
|
return [TextNode(type: "text", delta: delta)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -446,7 +446,7 @@ class NodesToHTMLConverter {
|
|||||||
childNodes.add(node);
|
childNodes.add(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final op in delta.operations) {
|
for (final op in delta) {
|
||||||
if (op is TextInsert) {
|
if (op is TextInsert) {
|
||||||
final attributes = op.attributes;
|
final attributes = op.attributes;
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
|
@ -94,7 +94,7 @@ class TransactionBuilder {
|
|||||||
() => Delta()
|
() => Delta()
|
||||||
..retain(firstOffset ?? firstLength)
|
..retain(firstOffset ?? firstLength)
|
||||||
..delete(firstLength - (firstOffset ?? firstLength))
|
..delete(firstLength - (firstOffset ?? firstLength))
|
||||||
..addAll(secondNode.delta.slice(secondOffset, secondLength).operations),
|
..addAll(secondNode.delta.slice(secondOffset, secondLength)),
|
||||||
);
|
);
|
||||||
afterSelection = Selection.collapsed(
|
afterSelection = Selection.collapsed(
|
||||||
Position(
|
Position(
|
||||||
@ -108,15 +108,14 @@ class TransactionBuilder {
|
|||||||
[Attributes? attributes]) {
|
[Attributes? attributes]) {
|
||||||
var newAttributes = attributes;
|
var newAttributes = attributes;
|
||||||
if (index != 0 && attributes == null) {
|
if (index != 0 && attributes == null) {
|
||||||
newAttributes = node.delta
|
newAttributes =
|
||||||
.slice(max(index - 1, 0), index)
|
node.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||||
.operations
|
|
||||||
.first
|
|
||||||
.attributes;
|
|
||||||
}
|
}
|
||||||
textEdit(
|
textEdit(
|
||||||
node,
|
node,
|
||||||
() => Delta().retain(index).insert(
|
() => Delta()
|
||||||
|
..retain(index)
|
||||||
|
..insert(
|
||||||
content,
|
content,
|
||||||
newAttributes,
|
newAttributes,
|
||||||
),
|
),
|
||||||
@ -126,12 +125,20 @@ class TransactionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
formatText(TextNode node, int index, int length, Attributes attributes) {
|
formatText(TextNode node, int index, int length, Attributes attributes) {
|
||||||
textEdit(node, () => Delta().retain(index).retain(length, attributes));
|
textEdit(
|
||||||
|
node,
|
||||||
|
() => Delta()
|
||||||
|
..retain(index)
|
||||||
|
..retain(length, attributes));
|
||||||
afterSelection = beforeSelection;
|
afterSelection = beforeSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteText(TextNode node, int index, int length) {
|
deleteText(TextNode node, int index, int length) {
|
||||||
textEdit(node, () => Delta().retain(index).delete(length));
|
textEdit(
|
||||||
|
node,
|
||||||
|
() => Delta()
|
||||||
|
..retain(index)
|
||||||
|
..delete(length));
|
||||||
afterSelection =
|
afterSelection =
|
||||||
Selection.collapsed(Position(path: node.path, offset: index));
|
Selection.collapsed(Position(path: node.path, offset: index));
|
||||||
}
|
}
|
||||||
@ -140,14 +147,17 @@ class TransactionBuilder {
|
|||||||
[Attributes? attributes]) {
|
[Attributes? attributes]) {
|
||||||
var newAttributes = attributes;
|
var newAttributes = attributes;
|
||||||
if (attributes == null) {
|
if (attributes == null) {
|
||||||
final ops = node.delta.slice(index, index + length).operations;
|
final ops = node.delta.slice(index, index + length);
|
||||||
if (ops.isNotEmpty) {
|
if (ops.isNotEmpty) {
|
||||||
newAttributes = ops.first.attributes;
|
newAttributes = ops.first.attributes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textEdit(
|
textEdit(
|
||||||
node,
|
node,
|
||||||
() => Delta().retain(index).delete(length).insert(content, newAttributes),
|
() => Delta()
|
||||||
|
..retain(index)
|
||||||
|
..delete(length)
|
||||||
|
..insert(content, newAttributes),
|
||||||
);
|
);
|
||||||
afterSelection = Selection.collapsed(
|
afterSelection = Selection.collapsed(
|
||||||
Position(
|
Position(
|
||||||
|
@ -198,7 +198,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextSpan get _textSpan => TextSpan(
|
TextSpan get _textSpan => TextSpan(
|
||||||
children: widget.textNode.delta.operations
|
children: widget.textNode.delta
|
||||||
.whereType<TextInsert>()
|
.whereType<TextInsert>()
|
||||||
.map((insert) => RichTextStyle(
|
.map((insert) => RichTextStyle(
|
||||||
attributes: insert.attributes ?? {},
|
attributes: insert.attributes ?? {},
|
||||||
|
@ -12,8 +12,8 @@ int _endOffsetOfNode(Node node) {
|
|||||||
|
|
||||||
extension on Position {
|
extension on Position {
|
||||||
Position? goLeft(EditorState editorState) {
|
Position? goLeft(EditorState editorState) {
|
||||||
if (offset == 0) {
|
|
||||||
final node = editorState.document.nodeAtPath(path)!;
|
final node = editorState.document.nodeAtPath(path)!;
|
||||||
|
if (offset == 0) {
|
||||||
final prevNode = node.previous;
|
final prevNode = node.previous;
|
||||||
if (prevNode != null) {
|
if (prevNode != null) {
|
||||||
return Position(
|
return Position(
|
||||||
@ -22,7 +22,11 @@ extension on Position {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Position(path: path, offset: offset - 1);
|
if (node is TextNode) {
|
||||||
|
return Position(path: path, offset: node.delta.prevRunePosition(offset));
|
||||||
|
} else {
|
||||||
|
return Position(path: path, offset: offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Position? goRight(EditorState editorState) {
|
Position? goRight(EditorState editorState) {
|
||||||
@ -36,7 +40,11 @@ extension on Position {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Position(path: path, offset: offset + 1);
|
if (node is TextNode) {
|
||||||
|
return Position(path: path, offset: node.delta.nextRunePosition(offset));
|
||||||
|
} else {
|
||||||
|
return Position(path: path, offset: offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ _pasteHTML(EditorState editorState, String html) {
|
|||||||
final textNodeAtPath = nodeAtPath as TextNode;
|
final textNodeAtPath = nodeAtPath as TextNode;
|
||||||
final firstTextNode = firstNode as TextNode;
|
final firstTextNode = firstNode as TextNode;
|
||||||
tb.textEdit(textNodeAtPath,
|
tb.textEdit(textNodeAtPath,
|
||||||
() => Delta().retain(startOffset).concat(firstTextNode.delta));
|
() => (Delta()..retain(startOffset)) + firstTextNode.delta);
|
||||||
tb.setAfterSelection(Selection.collapsed(Position(
|
tb.setAfterSelection(Selection.collapsed(Position(
|
||||||
path: path, offset: startOffset + firstTextNode.delta.length)));
|
path: path, offset: startOffset + firstTextNode.delta.length)));
|
||||||
tb.commit();
|
tb.commit();
|
||||||
@ -93,17 +93,18 @@ _pasteMultipleLinesInText(
|
|||||||
|
|
||||||
tb.textEdit(
|
tb.textEdit(
|
||||||
textNodeAtPath,
|
textNodeAtPath,
|
||||||
() => Delta()
|
() =>
|
||||||
.retain(offset)
|
(Delta()
|
||||||
.delete(remain.length)
|
..retain(offset)
|
||||||
.concat(firstTextNode.delta));
|
..delete(remain.length)) +
|
||||||
|
firstTextNode.delta);
|
||||||
|
|
||||||
final tailNodes = nodes.sublist(1);
|
final tailNodes = nodes.sublist(1);
|
||||||
path[path.length - 1]++;
|
path[path.length - 1]++;
|
||||||
if (tailNodes.isNotEmpty) {
|
if (tailNodes.isNotEmpty) {
|
||||||
if (tailNodes.last.type == "text") {
|
if (tailNodes.last.type == "text") {
|
||||||
final tailTextNode = tailNodes.last as TextNode;
|
final tailTextNode = tailNodes.last as TextNode;
|
||||||
tailTextNode.delta = tailTextNode.delta.concat(remain);
|
tailTextNode.delta = tailTextNode.delta + remain;
|
||||||
} else if (remain.length > 0) {
|
} else if (remain.length > 0) {
|
||||||
tailNodes.add(TextNode(type: "text", delta: remain));
|
tailNodes.add(TextNode(type: "text", delta: remain));
|
||||||
}
|
}
|
||||||
@ -151,7 +152,11 @@ _handlePastePlainText(EditorState editorState, String plainText) {
|
|||||||
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
editorState.document.nodeAtPath(selection.end.path)! as TextNode;
|
||||||
final beginOffset = selection.end.offset;
|
final beginOffset = selection.end.offset;
|
||||||
TransactionBuilder(editorState)
|
TransactionBuilder(editorState)
|
||||||
..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0]))
|
..textEdit(
|
||||||
|
node,
|
||||||
|
() => Delta()
|
||||||
|
..retain(beginOffset)
|
||||||
|
..insert(lines[0]))
|
||||||
..setAfterSelection(Selection.collapsed(Position(
|
..setAfterSelection(Selection.collapsed(Position(
|
||||||
path: selection.end.path, offset: beginOffset + lines[0].length)))
|
path: selection.end.path, offset: beginOffset + lines[0].length)))
|
||||||
..commit();
|
..commit();
|
||||||
@ -176,17 +181,19 @@ _handlePastePlainText(EditorState editorState, String plainText) {
|
|||||||
if (index++ == remains.length - 1) {
|
if (index++ == remains.length - 1) {
|
||||||
return TextNode(
|
return TextNode(
|
||||||
type: "text",
|
type: "text",
|
||||||
delta: Delta().insert(e).addAll(insertedLineSuffix.operations));
|
delta: Delta()
|
||||||
|
..insert(e)
|
||||||
|
..addAll(insertedLineSuffix));
|
||||||
}
|
}
|
||||||
return TextNode(type: "text", delta: Delta().insert(e));
|
return TextNode(type: "text", delta: Delta()..insert(e));
|
||||||
}).toList();
|
}).toList();
|
||||||
// insert first line
|
// insert first line
|
||||||
tb.textEdit(
|
tb.textEdit(
|
||||||
node,
|
node,
|
||||||
() => Delta()
|
() => Delta()
|
||||||
.retain(beginOffset)
|
..retain(beginOffset)
|
||||||
.insert(firstLine)
|
..insert(firstLine)
|
||||||
.delete(node.delta.length - beginOffset));
|
..delete(node.delta.length - beginOffset));
|
||||||
// insert remains
|
// insert remains
|
||||||
tb.insertNodes(path, nodes);
|
tb.insertNodes(path, nodes);
|
||||||
tb.commit();
|
tb.commit();
|
||||||
@ -227,7 +234,10 @@ _deleteSelectedContent(EditorState editorState) {
|
|||||||
final tb = TransactionBuilder(editorState);
|
final tb = TransactionBuilder(editorState);
|
||||||
final len = selection.end.offset - selection.start.offset;
|
final len = selection.end.offset - selection.start.offset;
|
||||||
tb.textEdit(
|
tb.textEdit(
|
||||||
textItem, () => Delta().retain(selection.start.offset).delete(len));
|
textItem,
|
||||||
|
() => Delta()
|
||||||
|
..retain(selection.start.offset)
|
||||||
|
..delete(len));
|
||||||
tb.setAfterSelection(Selection.collapsed(selection.start));
|
tb.setAfterSelection(Selection.collapsed(selection.start));
|
||||||
tb.commit();
|
tb.commit();
|
||||||
return;
|
return;
|
||||||
@ -241,12 +251,13 @@ _deleteSelectedContent(EditorState editorState) {
|
|||||||
final textItem = item as TextNode;
|
final textItem = item as TextNode;
|
||||||
final deleteLen = textItem.delta.length - selection.start.offset;
|
final deleteLen = textItem.delta.length - selection.start.offset;
|
||||||
tb.textEdit(textItem, () {
|
tb.textEdit(textItem, () {
|
||||||
final delta = Delta();
|
final delta = Delta()
|
||||||
delta.retain(selection.start.offset).delete(deleteLen);
|
..retain(selection.start.offset)
|
||||||
|
..delete(deleteLen);
|
||||||
|
|
||||||
if (endNode is TextNode) {
|
if (endNode is TextNode) {
|
||||||
final remain = endNode.delta.slice(selection.end.offset);
|
final remain = endNode.delta.slice(selection.end.offset);
|
||||||
delta.addAll(remain.operations);
|
delta.addAll(remain);
|
||||||
}
|
}
|
||||||
|
|
||||||
return delta;
|
return delta;
|
||||||
|
@ -25,7 +25,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
|||||||
TransactionBuilder transactionBuilder = TransactionBuilder(editorState);
|
TransactionBuilder transactionBuilder = TransactionBuilder(editorState);
|
||||||
if (textNodes.length == 1) {
|
if (textNodes.length == 1) {
|
||||||
final textNode = textNodes.first;
|
final textNode = textNodes.first;
|
||||||
final index = selection.start.offset - 1;
|
final index = textNode.delta.prevRunePosition(selection.start.offset);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
// 1. style
|
// 1. style
|
||||||
if (textNode.subtype != null) {
|
if (textNode.subtype != null) {
|
||||||
@ -62,8 +62,8 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
|||||||
if (selection.isCollapsed) {
|
if (selection.isCollapsed) {
|
||||||
transactionBuilder.deleteText(
|
transactionBuilder.deleteText(
|
||||||
textNode,
|
textNode,
|
||||||
selection.start.offset - 1,
|
index,
|
||||||
1,
|
selection.start.offset - index,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
transactionBuilder.deleteText(
|
transactionBuilder.deleteText(
|
||||||
|
@ -14,176 +14,218 @@ void main() {
|
|||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final death = Delta().retain(12).insert("White", {
|
final death = Delta()
|
||||||
|
..retain(12)
|
||||||
|
..insert("White", {
|
||||||
'color': '#fff',
|
'color': '#fff',
|
||||||
}).delete(4);
|
})
|
||||||
|
..delete(4);
|
||||||
|
|
||||||
final restores = delta.compose(death);
|
final restores = delta.compose(death);
|
||||||
expect(restores.operations, <TextOperation>[
|
expect(restores.toList(), <TextOperation>[
|
||||||
TextInsert('Gandalf', {'bold': true}),
|
TextInsert('Gandalf', {'bold': true}),
|
||||||
TextInsert(' the '),
|
TextInsert(' the '),
|
||||||
TextInsert('White', {'color': '#fff'}),
|
TextInsert('White', {'color': '#fff'}),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
test('compose()', () {
|
test('compose()', () {
|
||||||
final a = Delta().insert('A');
|
final a = Delta()..insert('A');
|
||||||
final b = Delta().insert('B');
|
final b = Delta()..insert('B');
|
||||||
final expected = Delta().insert('B').insert('A');
|
final expected = Delta()
|
||||||
|
..insert('B')
|
||||||
|
..insert('A');
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('insert + retain', () {
|
test('insert + retain', () {
|
||||||
final a = Delta().insert('A');
|
final a = Delta()..insert('A');
|
||||||
final b = Delta().retain(1, {
|
final b = Delta()
|
||||||
|
..retain(1, {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
final expected = Delta().insert('A', {
|
final expected = Delta()
|
||||||
|
..insert('A', {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('insert + delete', () {
|
test('insert + delete', () {
|
||||||
final a = Delta().insert('A');
|
final a = Delta()..insert('A');
|
||||||
final b = Delta().delete(1);
|
final b = Delta()..delete(1);
|
||||||
final expected = Delta();
|
final expected = Delta();
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('delete + insert', () {
|
test('delete + insert', () {
|
||||||
final a = Delta().delete(1);
|
final a = Delta()..delete(1);
|
||||||
final b = Delta().insert('B');
|
final b = Delta()..insert('B');
|
||||||
final expected = Delta().insert('B').delete(1);
|
final expected = Delta()
|
||||||
|
..insert('B')
|
||||||
|
..delete(1);
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('delete + retain', () {
|
test('delete + retain', () {
|
||||||
final a = Delta().delete(1);
|
final a = Delta()..delete(1);
|
||||||
final b = Delta().retain(1, {
|
final b = Delta()
|
||||||
|
..retain(1, {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
final expected = Delta().delete(1).retain(1, {
|
final expected = Delta()
|
||||||
|
..delete(1)
|
||||||
|
..retain(1, {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('delete + delete', () {
|
test('delete + delete', () {
|
||||||
final a = Delta().delete(1);
|
final a = Delta()..delete(1);
|
||||||
final b = Delta().delete(1);
|
final b = Delta()..delete(1);
|
||||||
final expected = Delta().delete(2);
|
final expected = Delta()..delete(2);
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain + insert', () {
|
test('retain + insert', () {
|
||||||
final a = Delta().retain(1, {'color': 'blue'});
|
final a = Delta()..retain(1, {'color': 'blue'});
|
||||||
final b = Delta().insert('B');
|
final b = Delta()..insert('B');
|
||||||
final expected = Delta().insert('B').retain(1, {
|
final expected = Delta()
|
||||||
|
..insert('B')
|
||||||
|
..retain(1, {
|
||||||
'color': 'blue',
|
'color': 'blue',
|
||||||
});
|
});
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain + retain', () {
|
test('retain + retain', () {
|
||||||
final a = Delta().retain(1, {
|
final a = Delta()
|
||||||
|
..retain(1, {
|
||||||
'color': 'blue',
|
'color': 'blue',
|
||||||
});
|
});
|
||||||
final b = Delta().retain(1, {
|
final b = Delta()
|
||||||
|
..retain(1, {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
final expected = Delta().retain(1, {
|
final expected = Delta()
|
||||||
|
..retain(1, {
|
||||||
'bold': true,
|
'bold': true,
|
||||||
'color': 'red',
|
'color': 'red',
|
||||||
});
|
});
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain + delete', () {
|
test('retain + delete', () {
|
||||||
final a = Delta().retain(1, {
|
final a = Delta()
|
||||||
|
..retain(1, {
|
||||||
'color': 'blue',
|
'color': 'blue',
|
||||||
});
|
});
|
||||||
final b = Delta().delete(1);
|
final b = Delta()..delete(1);
|
||||||
final expected = Delta().delete(1);
|
final expected = Delta()..delete(1);
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('insert in middle of text', () {
|
test('insert in middle of text', () {
|
||||||
final a = Delta().insert('Hello');
|
final a = Delta()..insert('Hello');
|
||||||
final b = Delta().retain(3).insert('X');
|
final b = Delta()
|
||||||
final expected = Delta().insert('HelXlo');
|
..retain(3)
|
||||||
|
..insert('X');
|
||||||
|
final expected = Delta()..insert('HelXlo');
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('insert and delete ordering', () {
|
test('insert and delete ordering', () {
|
||||||
final a = Delta().insert('Hello');
|
final a = Delta()..insert('Hello');
|
||||||
final b = Delta().insert('Hello');
|
final b = Delta()..insert('Hello');
|
||||||
final insertFirst = Delta().retain(3).insert('X').delete(1);
|
final insertFirst = Delta()
|
||||||
final deleteFirst = Delta().retain(3).delete(1).insert('X');
|
..retain(3)
|
||||||
final expected = Delta().insert('HelXo');
|
..insert('X')
|
||||||
|
..delete(1);
|
||||||
|
final deleteFirst = Delta()
|
||||||
|
..retain(3)
|
||||||
|
..delete(1)
|
||||||
|
..insert('X');
|
||||||
|
final expected = Delta()..insert('HelXo');
|
||||||
expect(a.compose(insertFirst), expected);
|
expect(a.compose(insertFirst), expected);
|
||||||
expect(b.compose(deleteFirst), expected);
|
expect(b.compose(deleteFirst), expected);
|
||||||
});
|
});
|
||||||
test('delete entire text', () {
|
test('delete entire text', () {
|
||||||
final a = Delta().retain(4).insert('Hello');
|
final a = Delta()
|
||||||
final b = Delta().delete(9);
|
..retain(4)
|
||||||
final expected = Delta().delete(4);
|
..insert('Hello');
|
||||||
|
final b = Delta()..delete(9);
|
||||||
|
final expected = Delta()..delete(4);
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain more than length of text', () {
|
test('retain more than length of text', () {
|
||||||
final a = Delta().insert('Hello');
|
final a = Delta()..insert('Hello');
|
||||||
final b = Delta().retain(10);
|
final b = Delta()..retain(10);
|
||||||
final expected = Delta().insert('Hello');
|
final expected = Delta()..insert('Hello');
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain start optimization', () {
|
test('retain start optimization', () {
|
||||||
final a = Delta()
|
final a = Delta()
|
||||||
.insert('A', {'bold': true})
|
..insert('A', {'bold': true})
|
||||||
.insert('B')
|
..insert('B')
|
||||||
.insert('C', {'bold': true})
|
..insert('C', {'bold': true})
|
||||||
.delete(1);
|
..delete(1);
|
||||||
final b = Delta().retain(3).insert('D');
|
final b = Delta()
|
||||||
|
..retain(3)
|
||||||
|
..insert('D');
|
||||||
final expected = Delta()
|
final expected = Delta()
|
||||||
.insert('A', {'bold': true})
|
..insert('A', {'bold': true})
|
||||||
.insert('B')
|
..insert('B')
|
||||||
.insert('C', {'bold': true})
|
..insert('C', {'bold': true})
|
||||||
.insert('D')
|
..insert('D')
|
||||||
.delete(1);
|
..delete(1);
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain end optimization', () {
|
test('retain end optimization', () {
|
||||||
final a = Delta()
|
final a = Delta()
|
||||||
.insert('A', {'bold': true})
|
..insert('A', {'bold': true})
|
||||||
.insert('B')
|
..insert('B')
|
||||||
.insert('C', {'bold': true});
|
..insert('C', {'bold': true});
|
||||||
final b = Delta().delete(1);
|
final b = Delta()..delete(1);
|
||||||
final expected = Delta().insert('B').insert('C', {'bold': true});
|
final expected = Delta()
|
||||||
|
..insert('B')
|
||||||
|
..insert('C', {'bold': true});
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
test('retain end optimization join', () {
|
test('retain end optimization join', () {
|
||||||
final a = Delta()
|
final a = Delta()
|
||||||
.insert('A', {'bold': true})
|
..insert('A', {'bold': true})
|
||||||
.insert('B')
|
..insert('B')
|
||||||
.insert('C', {'bold': true})
|
..insert('C', {'bold': true})
|
||||||
.insert('D')
|
..insert('D')
|
||||||
.insert('E', {'bold': true})
|
..insert('E', {'bold': true})
|
||||||
.insert('F');
|
..insert('F');
|
||||||
final b = Delta().retain(1).delete(1);
|
final b = Delta()
|
||||||
|
..retain(1)
|
||||||
|
..delete(1);
|
||||||
final expected = Delta()
|
final expected = Delta()
|
||||||
.insert('AC', {'bold': true})
|
..insert('AC', {'bold': true})
|
||||||
.insert('D')
|
..insert('D')
|
||||||
.insert('E', {'bold': true})
|
..insert('E', {'bold': true})
|
||||||
.insert('F');
|
..insert('F');
|
||||||
expect(a.compose(b), expected);
|
expect(a.compose(b), expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
group('invert', () {
|
group('invert', () {
|
||||||
test('insert', () {
|
test('insert', () {
|
||||||
final delta = Delta().retain(2).insert('A');
|
final delta = Delta()
|
||||||
final base = Delta().insert('12346');
|
..retain(2)
|
||||||
final expected = Delta().retain(2).delete(1);
|
..insert('A');
|
||||||
|
final base = Delta()..insert('12346');
|
||||||
|
final expected = Delta()
|
||||||
|
..retain(2)
|
||||||
|
..delete(1);
|
||||||
final inverted = delta.invert(base);
|
final inverted = delta.invert(base);
|
||||||
expect(expected, inverted);
|
expect(expected, inverted);
|
||||||
expect(base.compose(delta).compose(inverted), base);
|
expect(base.compose(delta).compose(inverted), base);
|
||||||
});
|
});
|
||||||
test('delete', () {
|
test('delete', () {
|
||||||
final delta = Delta().retain(2).delete(3);
|
final delta = Delta()
|
||||||
final base = Delta().insert('123456');
|
..retain(2)
|
||||||
final expected = Delta().retain(2).insert('345');
|
..delete(3);
|
||||||
|
final base = Delta()..insert('123456');
|
||||||
|
final expected = Delta()
|
||||||
|
..retain(2)
|
||||||
|
..insert('345');
|
||||||
final inverted = delta.invert(base);
|
final inverted = delta.invert(base);
|
||||||
expect(expected, inverted);
|
expect(expected, inverted);
|
||||||
expect(base.compose(delta).compose(inverted), base);
|
expect(base.compose(delta).compose(inverted), base);
|
||||||
@ -199,7 +241,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
group('json', () {
|
group('json', () {
|
||||||
test('toJson()', () {
|
test('toJson()', () {
|
||||||
final delta = Delta().retain(2).insert('A').delete(3);
|
final delta = Delta()
|
||||||
|
..retain(2)
|
||||||
|
..insert('A')
|
||||||
|
..delete(3);
|
||||||
expect(delta.toJson(), [
|
expect(delta.toJson(), [
|
||||||
{'retain': 2},
|
{'retain': 2},
|
||||||
{'insert': 'A'},
|
{'insert': 'A'},
|
||||||
@ -207,8 +252,9 @@ void main() {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
test('attributes', () {
|
test('attributes', () {
|
||||||
final delta =
|
final delta = Delta()
|
||||||
Delta().retain(2, {'bold': true}).insert('A', {'italic': true});
|
..retain(2, {'bold': true})
|
||||||
|
..insert('A', {'italic': true});
|
||||||
expect(delta.toJson(), [
|
expect(delta.toJson(), [
|
||||||
{
|
{
|
||||||
'retain': 2,
|
'retain': 2,
|
||||||
@ -226,8 +272,42 @@ void main() {
|
|||||||
{'insert': 'A'},
|
{'insert': 'A'},
|
||||||
{'delete': 3},
|
{'delete': 3},
|
||||||
]);
|
]);
|
||||||
final expected = Delta().retain(2).insert('A').delete(3);
|
final expected = Delta()
|
||||||
|
..retain(2)
|
||||||
|
..insert('A')
|
||||||
|
..delete(3);
|
||||||
expect(delta, expected);
|
expect(delta, expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
group('runes', () {
|
||||||
|
test("stringIndexes", () {
|
||||||
|
final indexes = stringIndexes('😊');
|
||||||
|
expect(indexes[0], 0);
|
||||||
|
expect(indexes[1], 0);
|
||||||
|
});
|
||||||
|
test("next rune 1", () {
|
||||||
|
final delta = Delta()..insert('😊');
|
||||||
|
expect(delta.nextRunePosition(0), 2);
|
||||||
|
});
|
||||||
|
test("next rune 2", () {
|
||||||
|
final delta = Delta()..insert('😊a');
|
||||||
|
expect(delta.nextRunePosition(0), 2);
|
||||||
|
});
|
||||||
|
test("next rune 3", () {
|
||||||
|
final delta = Delta()..insert('😊陈');
|
||||||
|
expect(delta.nextRunePosition(2), 3);
|
||||||
|
});
|
||||||
|
test("prev rune 1", () {
|
||||||
|
final delta = Delta()..insert('😊陈');
|
||||||
|
expect(delta.prevRunePosition(2), 0);
|
||||||
|
});
|
||||||
|
test("prev rune 2", () {
|
||||||
|
final delta = Delta()..insert('😊');
|
||||||
|
expect(delta.prevRunePosition(2), 0);
|
||||||
|
});
|
||||||
|
test("prev rune 3", () {
|
||||||
|
final delta = Delta()..insert('😊');
|
||||||
|
expect(delta.prevRunePosition(0), -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user