From db25b9c8d6f96e96432847b4f39daaedcf651129 Mon Sep 17 00:00:00 2001 From: li3317 Date: Fri, 6 Aug 2021 23:32:19 -0700 Subject: [PATCH] support inline image --- lib/src/models/documents/document.dart | 40 +++++--------- lib/src/models/rules/insert.dart | 31 ----------- lib/src/models/rules/rule.dart | 1 - lib/src/widgets/controller.dart | 5 +- lib/src/widgets/text_line.dart | 72 ++++++++++++++++++-------- 5 files changed, 65 insertions(+), 84 deletions(-) diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index d2303d15..9e6ce6b7 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -51,8 +51,7 @@ class Document { Stream> 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 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')); } diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 60211ab2..892b3e8a 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -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(); diff --git a/lib/src/models/rules/rule.dart b/lib/src/models/rules/rule.dart index 042f1aaa..a2649499 100644 --- a/lib/src/models/rules/rule.dart +++ b/lib/src/models/rules/rule.dart @@ -36,7 +36,6 @@ class Rules { const ResolveLineFormatRule(), const ResolveInlineFormatRule(), const InsertEmbedsRule(), - const ForceNewlineForInsertsAroundEmbedRule(), const AutoExitBlockRule(), const PreserveBlockStyleOnInsertRule(), const PreserveLineStyleOnSplitRule(), diff --git a/lib/src/widgets/controller.dart b/lib/src/widgets/controller.dart index 22763ca0..f26765b0 100644 --- a/lib/src/widgets/controller.dart +++ b/lib/src/widgets/controller.dart @@ -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 && diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index 049e73c8..4e664efd 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -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 = []; + var textNodes = LinkedList(); + for (final child in line.children) { + if (child is Embed) { + if (textNodes.isNotEmpty) { + textSpanChildren.add(_buildTextSpan(styles, textNodes, lineStyle)); + textNodes = LinkedList(); + } + // 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 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]; {