dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
7.2 KiB
320 lines
7.2 KiB
import 'dart:math' as math; |
|
|
|
import 'package:flutter_quill/models/documents/attribute.dart'; |
|
import 'package:flutter_quill/models/documents/nodes/node.dart'; |
|
import 'package:flutter_quill/models/quill_delta.dart'; |
|
|
|
import '../style.dart'; |
|
import 'block.dart'; |
|
import 'container.dart'; |
|
import 'embed.dart'; |
|
import 'leaf.dart'; |
|
|
|
class Line extends Container<Leaf?> { |
|
@override |
|
Leaf get defaultChild => Text(); |
|
|
|
@override |
|
int get length => super.length + 1; |
|
|
|
bool get hasEmbed { |
|
if (childCount != 1) { |
|
return false; |
|
} |
|
|
|
return children.single is Embed; |
|
} |
|
|
|
Line? get nextLine { |
|
if (!isLast) { |
|
return next is Block ? (next as Block).first as Line? : next as Line?; |
|
} |
|
if (parent is! Block) { |
|
return null; |
|
} |
|
|
|
if (parent!.isLast) { |
|
return null; |
|
} |
|
return parent!.next is Block |
|
? (parent!.next as Block).first as Line? |
|
: parent!.next as Line?; |
|
} |
|
|
|
@override |
|
Delta toDelta() { |
|
final delta = children |
|
.map((child) => child.toDelta()) |
|
.fold(Delta(), (dynamic a, b) => a.concat(b)); |
|
var attributes = style; |
|
if (parent is Block) { |
|
Block block = parent as Block; |
|
attributes = attributes.mergeAll(block.style); |
|
} |
|
delta.insert('\n', attributes.toJson()); |
|
return delta; |
|
} |
|
|
|
@override |
|
String toPlainText() => super.toPlainText() + '\n'; |
|
|
|
@override |
|
String toString() { |
|
final body = children.join(' → '); |
|
final styleString = style.isNotEmpty ? ' $style' : ''; |
|
return '¶ $body ⏎$styleString'; |
|
} |
|
|
|
@override |
|
void insert(int index, Object data, Style? style) { |
|
if (data is Embeddable) { |
|
_insert(index, data, style); |
|
return; |
|
} |
|
|
|
String text = data as String; |
|
int lineBreak = text.indexOf('\n'); |
|
if (lineBreak < 0) { |
|
_insert(index, text, style); |
|
return; |
|
} |
|
|
|
String prefix = text.substring(0, lineBreak); |
|
_insert(index, prefix, style); |
|
if (prefix.isNotEmpty) { |
|
index += prefix.length; |
|
} |
|
|
|
Line nextLine = _getNextLine(index); |
|
|
|
clearStyle(); |
|
|
|
if (parent is Block) { |
|
_unwrap(); |
|
} |
|
|
|
_format(style); |
|
|
|
// Continue with the remaining |
|
String remain = text.substring(lineBreak + 1); |
|
nextLine.insert(0, remain, style); |
|
} |
|
|
|
@override |
|
void retain(int index, int? len, Style? style) { |
|
if (style == null) { |
|
return; |
|
} |
|
int thisLen = length; |
|
|
|
int local = math.min(thisLen - index, len!); |
|
|
|
if (index + local == thisLen && local == 1) { |
|
assert(style.values.every((attr) => attr.scope == AttributeScope.BLOCK)); |
|
_format(style); |
|
} else { |
|
assert(style.values.every((attr) => attr.scope == AttributeScope.INLINE)); |
|
assert(index + local != thisLen); |
|
super.retain(index, local, style); |
|
} |
|
|
|
int remain = len - local; |
|
if (remain > 0) { |
|
assert(nextLine != null); |
|
nextLine!.retain(0, remain, style); |
|
} |
|
} |
|
|
|
@override |
|
void delete(int index, int? len) { |
|
int local = math.min(length - index, len!); |
|
bool deleted = index + local == length; |
|
if (deleted) { |
|
clearStyle(); |
|
if (local > 1) { |
|
super.delete(index, local - 1); |
|
} |
|
} else { |
|
super.delete(index, local); |
|
} |
|
|
|
int remain = len - local; |
|
if (remain > 0) { |
|
assert(nextLine != null); |
|
nextLine!.delete(0, remain); |
|
} |
|
|
|
if (deleted && isNotEmpty) { |
|
assert(nextLine != null); |
|
nextLine!.moveChildToNewParent(this); |
|
moveChildToNewParent(nextLine); |
|
} |
|
|
|
if (deleted) { |
|
Node p = parent!; |
|
unlink(); |
|
p.adjust(); |
|
} |
|
} |
|
|
|
void _format(Style? newStyle) { |
|
if (newStyle == null || newStyle.isEmpty) { |
|
return; |
|
} |
|
|
|
applyStyle(newStyle); |
|
Attribute? blockStyle = newStyle.getBlockExceptHeader(); |
|
if (blockStyle == null) { |
|
return; |
|
} |
|
|
|
if (parent is Block) { |
|
Attribute? parentStyle = (parent as Block).style.getBlockExceptHeader(); |
|
if (blockStyle.value == null) { |
|
_unwrap(); |
|
} else if (blockStyle != parentStyle) { |
|
_unwrap(); |
|
Block block = Block(); |
|
block.applyAttribute(blockStyle); |
|
_wrap(block); |
|
block.adjust(); |
|
} |
|
} else if (blockStyle.value != null) { |
|
Block block = Block(); |
|
block.applyAttribute(blockStyle); |
|
_wrap(block); |
|
block.adjust(); |
|
} |
|
} |
|
|
|
void _wrap(Block block) { |
|
assert(parent != null && parent is! Block); |
|
insertAfter(block); |
|
unlink(); |
|
block.add(this); |
|
} |
|
|
|
void _unwrap() { |
|
if (parent is! Block) { |
|
throw ArgumentError('Invalid parent'); |
|
} |
|
Block block = parent as Block; |
|
|
|
assert(block.children.contains(this)); |
|
|
|
if (isFirst) { |
|
unlink(); |
|
block.insertBefore(this); |
|
} else if (isLast) { |
|
unlink(); |
|
block.insertAfter(this); |
|
} else { |
|
Block before = block.clone() as Block; |
|
block.insertBefore(before); |
|
|
|
Line child = block.first as Line; |
|
while (child != this) { |
|
child.unlink(); |
|
before.add(child); |
|
child = block.first as Line; |
|
} |
|
unlink(); |
|
block.insertBefore(this); |
|
} |
|
block.adjust(); |
|
} |
|
|
|
Line _getNextLine(int index) { |
|
assert(index == 0 || (index > 0 && index < length)); |
|
|
|
Line line = clone() as Line; |
|
insertAfter(line); |
|
if (index == length - 1) { |
|
return line; |
|
} |
|
|
|
ChildQuery query = queryChild(index, false); |
|
while (!query.node!.isLast) { |
|
Leaf next = last as Leaf; |
|
next.unlink(); |
|
line.addFirst(next); |
|
} |
|
Leaf child = query.node as Leaf; |
|
Leaf? cut = child.splitAt(query.offset); |
|
cut?.unlink(); |
|
line.addFirst(cut); |
|
return line; |
|
} |
|
|
|
void _insert(int index, Object data, Style? style) { |
|
assert(index == 0 || (index > 0 && index < length)); |
|
|
|
if (data is String) { |
|
assert(!data.contains('\n')); |
|
if (data.isEmpty) { |
|
return; |
|
} |
|
} |
|
|
|
if (isNotEmpty) { |
|
ChildQuery result = queryChild(index, true); |
|
result.node!.insert(result.offset, data, style); |
|
return; |
|
} |
|
|
|
Leaf child = Leaf(data); |
|
add(child); |
|
child.format(style); |
|
} |
|
|
|
@override |
|
Node newInstance() { |
|
return Line(); |
|
} |
|
|
|
Style collectStyle(int offset, int len) { |
|
int local = math.min(length - offset, len); |
|
Style res = Style(); |
|
var excluded = <Attribute>{}; |
|
|
|
void _handle(Style style) { |
|
if (res.isEmpty) { |
|
excluded.addAll(style.values); |
|
} else { |
|
for (Attribute attr in res.values) { |
|
if (!style.containsKey(attr.key)) { |
|
excluded.add(attr); |
|
} |
|
} |
|
} |
|
Style remain = style.removeAll(excluded); |
|
res = res.removeAll(excluded); |
|
res = res.mergeAll(remain); |
|
} |
|
|
|
ChildQuery data = queryChild(offset, true); |
|
Leaf? node = data.node as Leaf?; |
|
if (node != null) { |
|
res = res.mergeAll(node.style); |
|
int pos = node.length - data.offset; |
|
while (!node!.isLast && pos < local) { |
|
node = node.next as Leaf?; |
|
_handle(node!.style); |
|
pos += node.length; |
|
} |
|
} |
|
|
|
res = res.mergeAll(style); |
|
if (parent is Block) { |
|
Block block = parent as Block; |
|
res = res.mergeAll(block.style); |
|
} |
|
|
|
int remain = len - local; |
|
if (remain > 0) { |
|
_handle(nextLine!.collectStyle(0, remain)); |
|
} |
|
|
|
return res; |
|
} |
|
}
|
|
|