From f56bbff75fc03a075437a00f745354f7e36fca97 Mon Sep 17 00:00:00 2001 From: X Code Date: Thu, 19 Aug 2021 01:09:34 -0700 Subject: [PATCH] Support for building custom inline styles (#351) --- lib/src/models/documents/attribute.dart | 8 +- lib/src/models/documents/style.dart | 3 +- lib/src/widgets/delegate.dart | 2 + lib/src/widgets/editor.dart | 139 ++++++++++++------------ lib/src/widgets/raw_editor.dart | 35 +++--- lib/src/widgets/simple_viewer.dart | 33 +++--- lib/src/widgets/text_block.dart | 35 +++--- lib/src/widgets/text_line.dart | 19 +++- 8 files changed, 151 insertions(+), 123 deletions(-) diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index acd979cb..97d82f15 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -165,11 +165,11 @@ class Attribute { Map toJson() => {key: value}; - static Attribute fromKeyValue(String key, dynamic value) { - if (!_registry.containsKey(key)) { - throw ArgumentError.value(key, 'key "$key" not found.'); + static Attribute? fromKeyValue(String key, dynamic value) { + final origin = _registry[key]; + if (origin == null) { + return null; } - final origin = _registry[key]!; final attribute = clone(origin, value); return attribute; } diff --git a/lib/src/models/documents/style.dart b/lib/src/models/documents/style.dart index fade1bb5..9ade3186 100644 --- a/lib/src/models/documents/style.dart +++ b/lib/src/models/documents/style.dart @@ -18,7 +18,8 @@ class Style { final result = attributes.map((key, dynamic value) { final attr = Attribute.fromKeyValue(key, value); - return MapEntry(key, attr); + return MapEntry( + key, attr ?? Attribute(key, AttributeScope.IGNORE, value)); }); return Style.attr(result); } diff --git a/lib/src/widgets/delegate.dart b/lib/src/widgets/delegate.dart index f22f66fc..0c9c8629 100644 --- a/lib/src/widgets/delegate.dart +++ b/lib/src/widgets/delegate.dart @@ -10,6 +10,8 @@ import 'text_selection.dart'; typedef EmbedBuilder = Widget Function( BuildContext context, Embed node, bool readOnly); +typedef StyleBuilder = TextStyle Function(String attributeKey); + abstract class EditorTextSelectionGestureDetectorBuilderDelegate { GlobalKey getEditableTextKey(); diff --git a/lib/src/widgets/editor.dart b/lib/src/widgets/editor.dart index c6d229fb..e55970c3 100644 --- a/lib/src/widgets/editor.dart +++ b/lib/src/widgets/editor.dart @@ -225,33 +225,35 @@ Widget _defaultEmbedBuilder( } class QuillEditor extends StatefulWidget { - const QuillEditor( - {required this.controller, - required this.focusNode, - required this.scrollController, - required this.scrollable, - required this.padding, - required this.autoFocus, - required this.readOnly, - required this.expands, - this.showCursor, - this.paintCursorAboveText, - this.placeholder, - this.enableInteractiveSelection = true, - this.scrollBottomInset = 0, - this.minHeight, - this.maxHeight, - this.customStyles, - this.textCapitalization = TextCapitalization.sentences, - this.keyboardAppearance = Brightness.light, - this.scrollPhysics, - this.onLaunchUrl, - this.onTapDown, - this.onTapUp, - this.onSingleLongTapStart, - this.onSingleLongTapMoveUpdate, - this.onSingleLongTapEnd, - this.embedBuilder = _defaultEmbedBuilder}); + const QuillEditor({ + required this.controller, + required this.focusNode, + required this.scrollController, + required this.scrollable, + required this.padding, + required this.autoFocus, + required this.readOnly, + required this.expands, + this.showCursor, + this.paintCursorAboveText, + this.placeholder, + this.enableInteractiveSelection = true, + this.scrollBottomInset = 0, + this.minHeight, + this.maxHeight, + this.customStyles, + this.textCapitalization = TextCapitalization.sentences, + this.keyboardAppearance = Brightness.light, + this.scrollPhysics, + this.onLaunchUrl, + this.onTapDown, + this.onTapUp, + this.onSingleLongTapStart, + this.onSingleLongTapMoveUpdate, + this.onSingleLongTapEnd, + this.embedBuilder = _defaultEmbedBuilder, + this.styleBuilder, + }); factory QuillEditor.basic({ required QuillController controller, @@ -310,6 +312,7 @@ class QuillEditor extends StatefulWidget { onSingleLongTapEnd; final EmbedBuilder embedBuilder; + final StyleBuilder? styleBuilder; @override _QuillEditorState createState() => _QuillEditorState(); @@ -374,46 +377,48 @@ class _QuillEditorState extends State return _selectionGestureDetectorBuilder.build( HitTestBehavior.translucent, RawEditor( - _editorKey, - widget.controller, - widget.focusNode, - widget.scrollController, - widget.scrollable, - widget.scrollBottomInset, - widget.padding, - widget.readOnly, - widget.placeholder, - widget.onLaunchUrl, - ToolbarOptions( - copy: widget.enableInteractiveSelection, - cut: widget.enableInteractiveSelection, - paste: widget.enableInteractiveSelection, - selectAll: widget.enableInteractiveSelection, - ), - theme.platform == TargetPlatform.iOS || - theme.platform == TargetPlatform.android, - widget.showCursor, - CursorStyle( - color: cursorColor, - backgroundColor: Colors.grey, - width: 2, - radius: cursorRadius, - offset: cursorOffset, - paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, - opacityAnimates: cursorOpacityAnimates, - ), - widget.textCapitalization, - widget.maxHeight, - widget.minHeight, - widget.customStyles, - widget.expands, - widget.autoFocus, - selectionColor, - textSelectionControls, - widget.keyboardAppearance, - widget.enableInteractiveSelection, - widget.scrollPhysics, - widget.embedBuilder), + _editorKey, + widget.controller, + widget.focusNode, + widget.scrollController, + widget.scrollable, + widget.scrollBottomInset, + widget.padding, + widget.readOnly, + widget.placeholder, + widget.onLaunchUrl, + ToolbarOptions( + copy: widget.enableInteractiveSelection, + cut: widget.enableInteractiveSelection, + paste: widget.enableInteractiveSelection, + selectAll: widget.enableInteractiveSelection, + ), + theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.android, + widget.showCursor, + CursorStyle( + color: cursorColor, + backgroundColor: Colors.grey, + width: 2, + radius: cursorRadius, + offset: cursorOffset, + paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, + opacityAnimates: cursorOpacityAnimates, + ), + widget.textCapitalization, + widget.maxHeight, + widget.minHeight, + widget.customStyles, + widget.expands, + widget.autoFocus, + selectionColor, + textSelectionControls, + widget.keyboardAppearance, + widget.enableInteractiveSelection, + widget.scrollPhysics, + widget.embedBuilder, + widget.styleBuilder, + ), ); } diff --git a/lib/src/widgets/raw_editor.dart b/lib/src/widgets/raw_editor.dart index fdfcec6b..2d121748 100644 --- a/lib/src/widgets/raw_editor.dart +++ b/lib/src/widgets/raw_editor.dart @@ -57,6 +57,7 @@ class RawEditor extends StatefulWidget { this.enableInteractiveSelection, this.scrollPhysics, this.embedBuilder, + this.styleBuilder, ) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, @@ -89,7 +90,7 @@ class RawEditor extends StatefulWidget { final bool enableInteractiveSelection; final ScrollPhysics? scrollPhysics; final EmbedBuilder embedBuilder; - + final StyleBuilder? styleBuilder; @override State createState() => RawEditorState(); } @@ -231,23 +232,24 @@ class RawEditorState extends EditorState } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - widget.selectionColor, - _styles, - widget.enableInteractiveSelection, - _hasFocus, - attrs.containsKey(Attribute.codeBlock.key) + block: node, + textDirection: _textDirection, + scrollBottomInset: widget.scrollBottomInset, + verticalSpacing: _getVerticalSpacingForBlock(node, _styles), + textSelection: widget.controller.selection, + color: widget.selectionColor, + styles: _styles, + enableInteractiveSelection: widget.enableInteractiveSelection, + hasFocus: _hasFocus, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) : null, - widget.embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap, - widget.readOnly); + embedBuilder: widget.embedBuilder, + cursorCont: _cursorCont, + indentLevelCounts: indentLevelCounts, + onCheckboxTap: _handleCheckboxTap, + readOnly: widget.readOnly, + styleBuilder: widget.styleBuilder); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); @@ -262,6 +264,7 @@ class RawEditorState extends EditorState line: node, textDirection: _textDirection, embedBuilder: widget.embedBuilder, + styleBuilder: widget.styleBuilder, styles: _styles!, readOnly: widget.readOnly, ); diff --git a/lib/src/widgets/simple_viewer.dart b/lib/src/widgets/simple_viewer.dart index 45b65502..2d5fa451 100644 --- a/lib/src/widgets/simple_viewer.dart +++ b/lib/src/widgets/simple_viewer.dart @@ -202,26 +202,23 @@ class _QuillSimpleViewerState extends State } else if (node is Block) { final attrs = node.style.attributes; final editableTextBlock = EditableTextBlock( - node, - _textDirection, - widget.scrollBottomInset, - _getVerticalSpacingForBlock(node, _styles), - widget.controller.selection, - Colors.black, - // selectionColor, - _styles, - false, - // enableInteractiveSelection, - false, - // hasFocus, - attrs.containsKey(Attribute.codeBlock.key) + block: node, + textDirection: _textDirection, + scrollBottomInset: widget.scrollBottomInset, + verticalSpacing: _getVerticalSpacingForBlock(node, _styles), + textSelection: widget.controller.selection, + color: Colors.black, + styles: _styles, + enableInteractiveSelection: false, + hasFocus: false, + contentPadding: attrs.containsKey(Attribute.codeBlock.key) ? const EdgeInsets.all(16) : null, - embedBuilder, - _cursorCont, - indentLevelCounts, - _handleCheckboxTap, - widget.readOnly); + embedBuilder: embedBuilder, + cursorCont: _cursorCont, + indentLevelCounts: indentLevelCounts, + onCheckboxTap: _handleCheckboxTap, + readOnly: widget.readOnly); result.add(editableTextBlock); } else { throw StateError('Unreachable.'); diff --git a/lib/src/widgets/text_block.dart b/lib/src/widgets/text_block.dart index 93130884..dc49e3ed 100644 --- a/lib/src/widgets/text_block.dart +++ b/lib/src/widgets/text_block.dart @@ -48,22 +48,23 @@ const List romanNumbers = [ class EditableTextBlock extends StatelessWidget { const EditableTextBlock( - this.block, - this.textDirection, - this.scrollBottomInset, - this.verticalSpacing, - this.textSelection, - this.color, - this.styles, - this.enableInteractiveSelection, - this.hasFocus, - this.contentPadding, - this.embedBuilder, - this.cursorCont, - this.indentLevelCounts, - this.onCheckboxTap, - this.readOnly, - ); + {required this.block, + required this.textDirection, + required this.scrollBottomInset, + required this.verticalSpacing, + required this.textSelection, + required this.color, + required this.styles, + required this.enableInteractiveSelection, + required this.hasFocus, + required this.contentPadding, + required this.embedBuilder, + required this.cursorCont, + required this.indentLevelCounts, + required this.onCheckboxTap, + required this.readOnly, + this.styleBuilder, + Key? key}); final Block block; final TextDirection textDirection; @@ -76,6 +77,7 @@ class EditableTextBlock extends StatelessWidget { final bool hasFocus; final EdgeInsets? contentPadding; final EmbedBuilder embedBuilder; + final StyleBuilder? styleBuilder; final CursorCont cursorCont; final Map indentLevelCounts; final Function(int, bool) onCheckboxTap; @@ -123,6 +125,7 @@ class EditableTextBlock extends StatelessWidget { line: line, textDirection: textDirection, embedBuilder: embedBuilder, + styleBuilder: styleBuilder, styles: styles!, readOnly: readOnly, ), diff --git a/lib/src/widgets/text_line.dart b/lib/src/widgets/text_line.dart index bf3c415c..e45a45b4 100644 --- a/lib/src/widgets/text_line.dart +++ b/lib/src/widgets/text_line.dart @@ -27,6 +27,7 @@ class TextLine extends StatelessWidget { required this.styles, required this.readOnly, this.textDirection, + this.styleBuilder, Key? key, }) : super(key: key); @@ -35,7 +36,7 @@ class TextLine extends StatelessWidget { final EmbedBuilder embedBuilder; final DefaultStyles styles; final bool readOnly; - + final StyleBuilder? styleBuilder; @override Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); @@ -149,10 +150,25 @@ class TextLine extends StatelessWidget { } textStyle = textStyle.merge(toMerge); + textStyle = _applyCustomAttributes(textStyle, line.style.attributes); return textStyle; } + TextStyle _applyCustomAttributes( + TextStyle textStyle, Map attributes) { + if (styleBuilder != null) { + attributes.keys + .where((key) => !attributes.containsKey(key)) + .forEach((key) { + /// Custom Attribute + final customAttr = styleBuilder!.call(key); + textStyle = textStyle.merge(customAttr); + }); + } + return textStyle; + } + TextSpan _getTextSpanFromNode(DefaultStyles defaultStyles, Node node) { final textNode = node as leaf.Text; final style = textNode.style; @@ -223,6 +239,7 @@ class TextLine extends StatelessWidget { res = res.merge(TextStyle(backgroundColor: backgroundColor)); } + res = _applyCustomAttributes(res, textNode.style.attributes); return TextSpan(text: textNode.value, style: res); }