Rich text editor for Flutter
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.
 
 
 
 
 

160 lines
4.4 KiB

import 'dart:collection';
import '../style.dart';
import 'leaf.dart';
import 'line.dart';
import 'node.dart';
/// Container can accommodate other nodes.
///
/// Delegates insert, retain and delete operations to children nodes. For each
/// operation container looks for a child at specified index position and
/// forwards operation to that child.
///
/// Most of the operation handling logic is implemented by [Line] and [Text].
abstract class Container<T extends Node?> extends Node {
final LinkedList<Node> _children = LinkedList<Node>();
/// List of children.
LinkedList<Node> get children => _children;
/// Returns total number of child nodes in this container.
///
/// To get text length of this container see [length].
int get childCount => _children.length;
/// Returns the first child [Node].
Node get first => _children.first;
/// Returns the last child [Node].
Node get last => _children.last;
/// Returns `true` if this container has no child nodes.
bool get isEmpty => _children.isEmpty;
/// Returns `true` if this container has at least 1 child.
bool get isNotEmpty => _children.isNotEmpty;
/// Returns an instance of default child for this container node.
///
/// Always returns fresh instance.
T get defaultChild;
/// Adds [node] to the end of this container children list.
void add(T node) {
assert(node?.parent == null);
node?.parent = this;
_children.add(node as Node);
}
/// Adds [node] to the beginning of this container children list.
void addFirst(T node) {
assert(node?.parent == null);
node?.parent = this;
_children.addFirst(node as Node);
}
/// Removes [node] from this container.
void remove(T node) {
assert(node?.parent == this);
node?.parent = null;
_children.remove(node as Node);
}
/// Moves children of this node to [newParent].
void moveChildToNewParent(Container? newParent) {
if (isEmpty) {
return;
}
final last = newParent!.isEmpty ? null : newParent.last as T?;
while (isNotEmpty) {
final child = first as T;
child?.unlink();
newParent.add(child);
}
/// In case [newParent] already had children we need to make sure
/// combined list is optimized.
if (last != null) last.adjust();
}
/// Queries the child [Node] at specified character [offset] in this container.
///
/// The result may contain the found node or `null` if no node is found
/// at specified offset.
///
/// [ChildQuery.offset] is set to relative offset within returned child node
/// which points at the same character position in the document as the
/// original [offset].
ChildQuery queryChild(int offset, bool inclusive) {
if (offset < 0 || offset > length) {
return ChildQuery(null, 0);
}
for (final node in children) {
final len = node.length;
if (offset < len || (inclusive && offset == len && (node.isLast))) {
return ChildQuery(node, offset);
}
offset -= len;
}
return ChildQuery(null, 0);
}
@override
String toPlainText() => children.map((child) => child.toPlainText()).join();
/// Content length of this node's children.
///
/// To get number of children in this node use [childCount].
@override
int get length => _children.fold(0, (cur, node) => cur + node.length);
@override
void insert(int index, Object data, Style? style) {
assert(index == 0 || (index > 0 && index < length));
if (isNotEmpty) {
final child = queryChild(index, false);
child.node!.insert(child.offset, data, style);
return;
}
// empty
assert(index == 0);
final node = defaultChild;
add(node);
node?.insert(index, data, style);
}
@override
void retain(int index, int? length, Style? attributes) {
assert(isNotEmpty);
final child = queryChild(index, false);
child.node!.retain(child.offset, length, attributes);
}
@override
void delete(int index, int? length) {
assert(isNotEmpty);
final child = queryChild(index, false);
child.node!.delete(child.offset, length);
}
@override
String toString() => _children.join('\n');
}
/// Result of a child query in a [Container].
class ChildQuery {
ChildQuery(this.node, this.offset);
/// The child node if found, otherwise `null`.
final Node? node;
/// Starting offset within the child [node] which points at the same
/// character in the document as the original offset passed to
/// [Container.queryChild] method.
final int offset;
}