Support for building custom inline styles (#351)

pull/371/head
X Code 4 years ago committed by GitHub
parent 9c40eb4e7a
commit f56bbff75f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      lib/src/models/documents/attribute.dart
  2. 3
      lib/src/models/documents/style.dart
  3. 2
      lib/src/widgets/delegate.dart
  4. 139
      lib/src/widgets/editor.dart
  5. 35
      lib/src/widgets/raw_editor.dart
  6. 33
      lib/src/widgets/simple_viewer.dart
  7. 35
      lib/src/widgets/text_block.dart
  8. 19
      lib/src/widgets/text_line.dart

@ -165,11 +165,11 @@ class Attribute<T> {
Map<String, dynamic> toJson() => <String, dynamic>{key: value}; Map<String, dynamic> toJson() => <String, dynamic>{key: value};
static Attribute fromKeyValue(String key, dynamic value) { static Attribute? fromKeyValue(String key, dynamic value) {
if (!_registry.containsKey(key)) { final origin = _registry[key];
throw ArgumentError.value(key, 'key "$key" not found.'); if (origin == null) {
return null;
} }
final origin = _registry[key]!;
final attribute = clone(origin, value); final attribute = clone(origin, value);
return attribute; return attribute;
} }

@ -18,7 +18,8 @@ class Style {
final result = attributes.map((key, dynamic value) { final result = attributes.map((key, dynamic value) {
final attr = Attribute.fromKeyValue(key, value); final attr = Attribute.fromKeyValue(key, value);
return MapEntry<String, Attribute>(key, attr); return MapEntry<String, Attribute>(
key, attr ?? Attribute(key, AttributeScope.IGNORE, value));
}); });
return Style.attr(result); return Style.attr(result);
} }

@ -10,6 +10,8 @@ import 'text_selection.dart';
typedef EmbedBuilder = Widget Function( typedef EmbedBuilder = Widget Function(
BuildContext context, Embed node, bool readOnly); BuildContext context, Embed node, bool readOnly);
typedef StyleBuilder = TextStyle Function(String attributeKey);
abstract class EditorTextSelectionGestureDetectorBuilderDelegate { abstract class EditorTextSelectionGestureDetectorBuilderDelegate {
GlobalKey<EditorState> getEditableTextKey(); GlobalKey<EditorState> getEditableTextKey();

@ -225,33 +225,35 @@ Widget _defaultEmbedBuilder(
} }
class QuillEditor extends StatefulWidget { class QuillEditor extends StatefulWidget {
const QuillEditor( const QuillEditor({
{required this.controller, required this.controller,
required this.focusNode, required this.focusNode,
required this.scrollController, required this.scrollController,
required this.scrollable, required this.scrollable,
required this.padding, required this.padding,
required this.autoFocus, required this.autoFocus,
required this.readOnly, required this.readOnly,
required this.expands, required this.expands,
this.showCursor, this.showCursor,
this.paintCursorAboveText, this.paintCursorAboveText,
this.placeholder, this.placeholder,
this.enableInteractiveSelection = true, this.enableInteractiveSelection = true,
this.scrollBottomInset = 0, this.scrollBottomInset = 0,
this.minHeight, this.minHeight,
this.maxHeight, this.maxHeight,
this.customStyles, this.customStyles,
this.textCapitalization = TextCapitalization.sentences, this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light, this.keyboardAppearance = Brightness.light,
this.scrollPhysics, this.scrollPhysics,
this.onLaunchUrl, this.onLaunchUrl,
this.onTapDown, this.onTapDown,
this.onTapUp, this.onTapUp,
this.onSingleLongTapStart, this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate, this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd, this.onSingleLongTapEnd,
this.embedBuilder = _defaultEmbedBuilder}); this.embedBuilder = _defaultEmbedBuilder,
this.styleBuilder,
});
factory QuillEditor.basic({ factory QuillEditor.basic({
required QuillController controller, required QuillController controller,
@ -310,6 +312,7 @@ class QuillEditor extends StatefulWidget {
onSingleLongTapEnd; onSingleLongTapEnd;
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
final StyleBuilder? styleBuilder;
@override @override
_QuillEditorState createState() => _QuillEditorState(); _QuillEditorState createState() => _QuillEditorState();
@ -374,46 +377,48 @@ class _QuillEditorState extends State<QuillEditor>
return _selectionGestureDetectorBuilder.build( return _selectionGestureDetectorBuilder.build(
HitTestBehavior.translucent, HitTestBehavior.translucent,
RawEditor( RawEditor(
_editorKey, _editorKey,
widget.controller, widget.controller,
widget.focusNode, widget.focusNode,
widget.scrollController, widget.scrollController,
widget.scrollable, widget.scrollable,
widget.scrollBottomInset, widget.scrollBottomInset,
widget.padding, widget.padding,
widget.readOnly, widget.readOnly,
widget.placeholder, widget.placeholder,
widget.onLaunchUrl, widget.onLaunchUrl,
ToolbarOptions( ToolbarOptions(
copy: widget.enableInteractiveSelection, copy: widget.enableInteractiveSelection,
cut: widget.enableInteractiveSelection, cut: widget.enableInteractiveSelection,
paste: widget.enableInteractiveSelection, paste: widget.enableInteractiveSelection,
selectAll: widget.enableInteractiveSelection, selectAll: widget.enableInteractiveSelection,
), ),
theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.iOS ||
theme.platform == TargetPlatform.android, theme.platform == TargetPlatform.android,
widget.showCursor, widget.showCursor,
CursorStyle( CursorStyle(
color: cursorColor, color: cursorColor,
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
width: 2, width: 2,
radius: cursorRadius, radius: cursorRadius,
offset: cursorOffset, offset: cursorOffset,
paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText, paintAboveText: widget.paintCursorAboveText ?? paintCursorAboveText,
opacityAnimates: cursorOpacityAnimates, opacityAnimates: cursorOpacityAnimates,
), ),
widget.textCapitalization, widget.textCapitalization,
widget.maxHeight, widget.maxHeight,
widget.minHeight, widget.minHeight,
widget.customStyles, widget.customStyles,
widget.expands, widget.expands,
widget.autoFocus, widget.autoFocus,
selectionColor, selectionColor,
textSelectionControls, textSelectionControls,
widget.keyboardAppearance, widget.keyboardAppearance,
widget.enableInteractiveSelection, widget.enableInteractiveSelection,
widget.scrollPhysics, widget.scrollPhysics,
widget.embedBuilder), widget.embedBuilder,
widget.styleBuilder,
),
); );
} }

@ -57,6 +57,7 @@ class RawEditor extends StatefulWidget {
this.enableInteractiveSelection, this.enableInteractiveSelection,
this.scrollPhysics, this.scrollPhysics,
this.embedBuilder, this.embedBuilder,
this.styleBuilder,
) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'), ) : assert(maxHeight == null || maxHeight > 0, 'maxHeight cannot be null'),
assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'), assert(minHeight == null || minHeight >= 0, 'minHeight cannot be null'),
assert(maxHeight == null || minHeight == null || maxHeight >= minHeight, assert(maxHeight == null || minHeight == null || maxHeight >= minHeight,
@ -89,7 +90,7 @@ class RawEditor extends StatefulWidget {
final bool enableInteractiveSelection; final bool enableInteractiveSelection;
final ScrollPhysics? scrollPhysics; final ScrollPhysics? scrollPhysics;
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
final StyleBuilder? styleBuilder;
@override @override
State<StatefulWidget> createState() => RawEditorState(); State<StatefulWidget> createState() => RawEditorState();
} }
@ -231,23 +232,24 @@ class RawEditorState extends EditorState
} else if (node is Block) { } else if (node is Block) {
final attrs = node.style.attributes; final attrs = node.style.attributes;
final editableTextBlock = EditableTextBlock( final editableTextBlock = EditableTextBlock(
node, block: node,
_textDirection, textDirection: _textDirection,
widget.scrollBottomInset, scrollBottomInset: widget.scrollBottomInset,
_getVerticalSpacingForBlock(node, _styles), verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
widget.controller.selection, textSelection: widget.controller.selection,
widget.selectionColor, color: widget.selectionColor,
_styles, styles: _styles,
widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
_hasFocus, hasFocus: _hasFocus,
attrs.containsKey(Attribute.codeBlock.key) contentPadding: attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16) ? const EdgeInsets.all(16)
: null, : null,
widget.embedBuilder, embedBuilder: widget.embedBuilder,
_cursorCont, cursorCont: _cursorCont,
indentLevelCounts, indentLevelCounts: indentLevelCounts,
_handleCheckboxTap, onCheckboxTap: _handleCheckboxTap,
widget.readOnly); readOnly: widget.readOnly,
styleBuilder: widget.styleBuilder);
result.add(editableTextBlock); result.add(editableTextBlock);
} else { } else {
throw StateError('Unreachable.'); throw StateError('Unreachable.');
@ -262,6 +264,7 @@ class RawEditorState extends EditorState
line: node, line: node,
textDirection: _textDirection, textDirection: _textDirection,
embedBuilder: widget.embedBuilder, embedBuilder: widget.embedBuilder,
styleBuilder: widget.styleBuilder,
styles: _styles!, styles: _styles!,
readOnly: widget.readOnly, readOnly: widget.readOnly,
); );

@ -202,26 +202,23 @@ class _QuillSimpleViewerState extends State<QuillSimpleViewer>
} else if (node is Block) { } else if (node is Block) {
final attrs = node.style.attributes; final attrs = node.style.attributes;
final editableTextBlock = EditableTextBlock( final editableTextBlock = EditableTextBlock(
node, block: node,
_textDirection, textDirection: _textDirection,
widget.scrollBottomInset, scrollBottomInset: widget.scrollBottomInset,
_getVerticalSpacingForBlock(node, _styles), verticalSpacing: _getVerticalSpacingForBlock(node, _styles),
widget.controller.selection, textSelection: widget.controller.selection,
Colors.black, color: Colors.black,
// selectionColor, styles: _styles,
_styles, enableInteractiveSelection: false,
false, hasFocus: false,
// enableInteractiveSelection, contentPadding: attrs.containsKey(Attribute.codeBlock.key)
false,
// hasFocus,
attrs.containsKey(Attribute.codeBlock.key)
? const EdgeInsets.all(16) ? const EdgeInsets.all(16)
: null, : null,
embedBuilder, embedBuilder: embedBuilder,
_cursorCont, cursorCont: _cursorCont,
indentLevelCounts, indentLevelCounts: indentLevelCounts,
_handleCheckboxTap, onCheckboxTap: _handleCheckboxTap,
widget.readOnly); readOnly: widget.readOnly);
result.add(editableTextBlock); result.add(editableTextBlock);
} else { } else {
throw StateError('Unreachable.'); throw StateError('Unreachable.');

@ -48,22 +48,23 @@ const List<String> romanNumbers = [
class EditableTextBlock extends StatelessWidget { class EditableTextBlock extends StatelessWidget {
const EditableTextBlock( const EditableTextBlock(
this.block, {required this.block,
this.textDirection, required this.textDirection,
this.scrollBottomInset, required this.scrollBottomInset,
this.verticalSpacing, required this.verticalSpacing,
this.textSelection, required this.textSelection,
this.color, required this.color,
this.styles, required this.styles,
this.enableInteractiveSelection, required this.enableInteractiveSelection,
this.hasFocus, required this.hasFocus,
this.contentPadding, required this.contentPadding,
this.embedBuilder, required this.embedBuilder,
this.cursorCont, required this.cursorCont,
this.indentLevelCounts, required this.indentLevelCounts,
this.onCheckboxTap, required this.onCheckboxTap,
this.readOnly, required this.readOnly,
); this.styleBuilder,
Key? key});
final Block block; final Block block;
final TextDirection textDirection; final TextDirection textDirection;
@ -76,6 +77,7 @@ class EditableTextBlock extends StatelessWidget {
final bool hasFocus; final bool hasFocus;
final EdgeInsets? contentPadding; final EdgeInsets? contentPadding;
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
final StyleBuilder? styleBuilder;
final CursorCont cursorCont; final CursorCont cursorCont;
final Map<int, int> indentLevelCounts; final Map<int, int> indentLevelCounts;
final Function(int, bool) onCheckboxTap; final Function(int, bool) onCheckboxTap;
@ -123,6 +125,7 @@ class EditableTextBlock extends StatelessWidget {
line: line, line: line,
textDirection: textDirection, textDirection: textDirection,
embedBuilder: embedBuilder, embedBuilder: embedBuilder,
styleBuilder: styleBuilder,
styles: styles!, styles: styles!,
readOnly: readOnly, readOnly: readOnly,
), ),

@ -27,6 +27,7 @@ class TextLine extends StatelessWidget {
required this.styles, required this.styles,
required this.readOnly, required this.readOnly,
this.textDirection, this.textDirection,
this.styleBuilder,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -35,7 +36,7 @@ class TextLine extends StatelessWidget {
final EmbedBuilder embedBuilder; final EmbedBuilder embedBuilder;
final DefaultStyles styles; final DefaultStyles styles;
final bool readOnly; final bool readOnly;
final StyleBuilder? styleBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
@ -149,10 +150,25 @@ class TextLine extends StatelessWidget {
} }
textStyle = textStyle.merge(toMerge); textStyle = textStyle.merge(toMerge);
textStyle = _applyCustomAttributes(textStyle, line.style.attributes);
return textStyle; return textStyle;
} }
TextStyle _applyCustomAttributes(
TextStyle textStyle, Map<String, Attribute> 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) { 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;
@ -223,6 +239,7 @@ class TextLine extends StatelessWidget {
res = res.merge(TextStyle(backgroundColor: backgroundColor)); res = res.merge(TextStyle(backgroundColor: backgroundColor));
} }
res = _applyCustomAttributes(res, textNode.style.attributes);
return TextSpan(text: textNode.value, style: res); return TextSpan(text: textNode.value, style: res);
} }

Loading…
Cancel
Save