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

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

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

@ -1,3 +1,4 @@
import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -38,22 +39,12 @@ class TextLine extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
if (line.hasEmbed && line.childCount == 1) {
if (line.hasEmbed) { // For video, it is always single child
if (line.childCount == 1) { final embed = line.children.single as Embed;
// 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;
return EmbedProxy(embedBuilder(context, embed, readOnly)); return EmbedProxy(embedBuilder(context, embed, readOnly));
} }
final textSpan = _getTextSpanForWholeLine(context);
final textSpan = _buildTextSpan(context);
final strutStyle = StrutStyle.fromTextStyle(textSpan.style!); final strutStyle = StrutStyle.fromTextStyle(textSpan.style!);
final textAlign = _getTextAlign(); final textAlign = _getTextAlign();
final child = RichText( final child = RichText(
@ -75,6 +66,42 @@ class TextLine extends StatelessWidget {
null); 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() { TextAlign _getTextAlign() {
final alignment = line.style.attributes[Attribute.align.key]; final alignment = line.style.attributes[Attribute.align.key];
if (alignment == Attribute.leftAlignment) { if (alignment == Attribute.leftAlignment) {
@ -89,17 +116,20 @@ class TextLine extends StatelessWidget {
return TextAlign.start; return TextAlign.start;
} }
TextSpan _buildTextSpan(BuildContext context) { TextSpan _buildTextSpan(DefaultStyles defaultStyles, LinkedList<Node> nodes,
final defaultStyles = styles; TextStyle lineStyle) {
final children = line.children final children = nodes
.map((node) => _getTextSpanFromNode(defaultStyles, node)) .map((node) => _getTextSpanFromNode(defaultStyles, node))
.toList(growable: false); .toList(growable: false);
return TextSpan(children: children, style: lineStyle);
}
TextStyle _getLineStyle(DefaultStyles defaultStyles) {
var textStyle = const TextStyle(); var textStyle = const TextStyle();
if (line.style.containsKey(Attribute.placeholder.key)) { if (line.style.containsKey(Attribute.placeholder.key)) {
textStyle = defaultStyles.placeHolder!.style; return defaultStyles.placeHolder!.style;
return TextSpan(children: children, style: textStyle);
} }
final header = line.style.attributes[Attribute.header.key]; final header = line.style.attributes[Attribute.header.key];
@ -123,13 +153,13 @@ class TextLine extends StatelessWidget {
textStyle = textStyle.merge(toMerge); textStyle = textStyle.merge(toMerge);
return TextSpan(children: children, style: textStyle); return textStyle;
} }
TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) {
final textNode = node as leaf.Text; final textNode = node as leaf.Text;
final style = textNode.style; 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]; final color = textNode.style.attributes[Attribute.color.key];
<String, TextStyle?>{ <String, TextStyle?>{

Loading…
Cancel
Save