support inline image

pull/332/head
li3317 4 years ago
parent 0a409ae3d5
commit db25b9c8d6
  1. 40
      lib/src/models/documents/document.dart
  2. 31
      lib/src/models/rules/insert.dart
  3. 1
      lib/src/models/rules/rule.dart
  4. 5
      lib/src/widgets/controller.dart
  5. 72
      lib/src/widgets/text_line.dart

@ -51,8 +51,7 @@ class Document {
Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => _observer.stream;
Delta insert(int index, Object? data,
{int replaceLength = 0, bool autoAppendNewlineAfterImage = true}) {
Delta insert(int index, Object? data, {int replaceLength = 0}) {
assert(index >= 0);
assert(data is String || data is Embeddable);
if (data is Embeddable) {
@ -63,8 +62,7 @@ class Document {
final delta = _rules.apply(RuleType.INSERT, this, index,
data: data, len: replaceLength);
compose(delta, ChangeSource.LOCAL,
autoAppendNewlineAfterImage: autoAppendNewlineAfterImage);
compose(delta, ChangeSource.LOCAL);
return delta;
}
@ -77,8 +75,7 @@ class Document {
return delta;
}
Delta replace(int index, int len, Object? data,
{bool autoAppendNewlineAfterImage = true}) {
Delta replace(int index, int len, Object? data) {
assert(index >= 0);
assert(data is String || data is Embeddable);
@ -91,9 +88,7 @@ class Document {
// We have to insert before applying delete rules
// Otherwise delete would be operating on stale document snapshot.
if (dataIsNotEmpty) {
delta = insert(index, data,
replaceLength: len,
autoAppendNewlineAfterImage: autoAppendNewlineAfterImage);
delta = insert(index, data, replaceLength: len);
}
if (len > 0) {
@ -141,17 +136,13 @@ class Document {
return block.queryChild(res.offset, true);
}
void compose(Delta delta, ChangeSource changeSource,
{bool autoAppendNewlineAfterImage = true,
bool autoAppendNewlineAfterVideo = true}) {
void compose(Delta delta, ChangeSource changeSource) {
assert(!_observer.isClosed);
delta.trim();
assert(delta.isNotEmpty);
var offset = 0;
delta = _transform(delta,
autoAppendNewlineAfterImage: autoAppendNewlineAfterImage,
autoAppendNewlineAfterVideo: autoAppendNewlineAfterVideo);
delta = _transform(delta);
final originalDelta = toDelta();
for (final op in delta.toList()) {
final style =
@ -195,44 +186,37 @@ class Document {
bool get hasRedo => _history.hasRedo;
static Delta _transform(Delta delta,
{bool autoAppendNewlineAfterImage = true,
bool autoAppendNewlineAfterVideo = true}) {
static Delta _transform(Delta delta) {
final res = Delta();
final ops = delta.toList();
for (var i = 0; i < ops.length; i++) {
final op = ops[i];
res.push(op);
if (autoAppendNewlineAfterImage) {
_autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'image');
}
if (autoAppendNewlineAfterVideo) {
_autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'video');
}
_autoAppendNewlineAfterEmbeddable(i, ops, op, res, 'video');
}
return res;
}
static void _autoAppendNewlineAfterEmbeddable(
int i, List<Operation> ops, Operation op, Delta res, String type) {
final nextOpIsImage = i + 1 < ops.length &&
final nextOpIsEmbed = i + 1 < ops.length &&
ops[i + 1].isInsert &&
ops[i + 1].data is Map &&
(ops[i + 1].data as Map).containsKey(type);
if (nextOpIsImage &&
if (nextOpIsEmbed &&
op.data is String &&
(op.data as String).isNotEmpty &&
!(op.data as String).endsWith('\n')) {
res.push(Operation.insert('\n'));
}
// embed could be image or video
final opInsertImage =
final opInsertEmbed =
op.isInsert && op.data is Map && (op.data as Map).containsKey(type);
final nextOpIsLineBreak = i + 1 < ops.length &&
ops[i + 1].isInsert &&
ops[i + 1].data is String &&
(ops[i + 1].data as String).startsWith('\n');
if (opInsertImage && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
if (opInsertEmbed && (i + 1 == ops.length - 1 || !nextOpIsLineBreak)) {
// automatically append '\n' for embeddable
res.push(Operation.insert('\n'));
}

@ -273,37 +273,6 @@ class InsertEmbedsRule extends InsertRule {
}
}
class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
const ForceNewlineForInsertsAroundEmbedRule();
@override
Delta? applyRule(Delta document, int index,
{int? len, Object? data, Attribute? attribute}) {
if (data is! String) {
return null;
}
final text = data;
final itr = DeltaIterator(document);
final prev = itr.skip(index);
final cur = itr.next();
final cursorBeforeEmbed = cur.data is! String;
final cursorAfterEmbed = prev != null && prev.data is! String;
if (!cursorBeforeEmbed && !cursorAfterEmbed) {
return null;
}
final delta = Delta()..retain(index + (len ?? 0));
if (cursorBeforeEmbed && !text.endsWith('\n')) {
return delta..insert(text)..insert('\n');
}
if (cursorAfterEmbed && !text.startsWith('\n')) {
return delta..insert('\n')..insert(text);
}
return delta..insert(text);
}
}
class AutoFormatLinksRule extends InsertRule {
const AutoFormatLinksRule();

@ -36,7 +36,6 @@ class Rules {
const ResolveLineFormatRule(),
const ResolveInlineFormatRule(),
const InsertEmbedsRule(),
const ForceNewlineForInsertsAroundEmbedRule(),
const AutoExitBlockRule(),
const PreserveBlockStyleOnInsertRule(),
const PreserveLineStyleOnSplitRule(),

@ -106,13 +106,12 @@ class QuillController extends ChangeNotifier {
void replaceText(
int index, int len, Object? data, TextSelection? textSelection,
{bool ignoreFocus = false, bool autoAppendNewlineAfterImage = true}) {
{bool ignoreFocus = false}) {
assert(data is String || data is Embeddable);
Delta? delta;
if (len > 0 || data is! String || data.isNotEmpty) {
delta = document.replace(index, len, data,
autoAppendNewlineAfterImage: autoAppendNewlineAfterImage);
delta = document.replace(index, len, data);
var shouldRetainDelta = toggledStyle.isNotEmpty &&
delta.isNotEmpty &&
delta.length <= 2 &&

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
@ -38,22 +39,12 @@ class TextLine extends StatelessWidget {
@override
Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context));
if (line.hasEmbed) {
if (line.childCount == 1) {
// For video, it is always single child
final embed = line.children.single as Embed;
return EmbedProxy(embedBuilder(context, embed, readOnly));
}
// The line could contain more than one Embed & more than one Text
// TODO: handle more than one Embed
final embed =
line.children.firstWhere((child) => child is Embed) as Embed;
if (line.hasEmbed && line.childCount == 1) {
// For video, it is always single child
final embed = line.children.single as Embed;
return EmbedProxy(embedBuilder(context, embed, readOnly));
}
final textSpan = _buildTextSpan(context);
final textSpan = _getTextSpanForWholeLine(context);
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
final textAlign = _getTextAlign();
final child = RichText(
@ -75,6 +66,42 @@ class TextLine extends StatelessWidget {
null);
}
InlineSpan _getTextSpanForWholeLine(BuildContext context) {
final lineStyle = _getLineStyle(styles);
if (!line.hasEmbed) {
return _buildTextSpan(styles, line.children, lineStyle);
}
// The line could contain more than one Embed & more than one Text
final textSpanChildren = <InlineSpan>[];
var textNodes = LinkedList<Node>();
for (final child in line.children) {
if (child is Embed) {
if (textNodes.isNotEmpty) {
textSpanChildren.add(_buildTextSpan(styles, textNodes, lineStyle));
textNodes = LinkedList<Node>();
}
// Here it should be image
final embed = WidgetSpan(
child: EmbedProxy(embedBuilder(context, child, readOnly)));
textSpanChildren.add(embed);
continue;
}
// here child is Text node and its value is cloned
textNodes.add(child.clone());
}
if (textNodes.isNotEmpty) {
textSpanChildren.add(_buildTextSpan(styles, textNodes, lineStyle));
}
return TextSpan(style: lineStyle, children: textSpanChildren);
}
// test with different combinations
// comb through logic , see if any refactoring is needed
TextAlign _getTextAlign() {
final alignment = line.style.attributes[Attribute.align.key];
if (alignment == Attribute.leftAlignment) {
@ -89,17 +116,20 @@ class TextLine extends StatelessWidget {
return TextAlign.start;
}
TextSpan _buildTextSpan(BuildContext context) {
final defaultStyles = styles;
final children = line.children
TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList<Node> nodes,
TextStyle lineStyle) {
final children = nodes
.map((node) => _getTextSpanFromNode(defaultStyles, node))
.toList(growable: false);
return TextSpan(children: children, style: lineStyle);
}
TextStyle _getLineStyle(DefaultStyles defaultStyles) {
var textStyle = const TextStyle();
if (line.style.containsKey(Attribute.placeholder.key)) {
textStyle = defaultStyles.placeHolder!.style;
return TextSpan(children: children, style: textStyle);
return defaultStyles.placeHolder!.style;
}
final header = line.style.attributes[Attribute.header.key];
@ -123,13 +153,13 @@ class TextLine extends StatelessWidget {
textStyle = textStyle.merge(toMerge);
return TextSpan(children: children, style: textStyle);
return textStyle;
}
TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) {
final textNode = node as leaf.Text;
final style = textNode.style;
var res = const TextStyle();
var res = const TextStyle(); // This is inline text style
final color = textNode.style.attributes[Attribute.color.key];
<String, TextStyle?>{

Loading…
Cancel
Save