parent
e49421f48c
commit
760b4def7e
1 changed files with 337 additions and 0 deletions
@ -0,0 +1,337 @@ |
||||
import 'dart:convert'; |
||||
import 'dart:io' as io; |
||||
|
||||
import 'package:flutter/foundation.dart'; |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/rendering.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
import 'package:string_validator/string_validator.dart'; |
||||
import 'package:tuple/tuple.dart'; |
||||
|
||||
import '../models/documents/attribute.dart'; |
||||
import '../models/documents/document.dart'; |
||||
import '../models/documents/nodes/block.dart'; |
||||
import '../models/documents/nodes/leaf.dart' as leaf; |
||||
import '../models/documents/nodes/line.dart'; |
||||
import 'controller.dart'; |
||||
import 'cursor.dart'; |
||||
import 'default_styles.dart'; |
||||
import 'delegate.dart'; |
||||
import 'editor.dart'; |
||||
import 'text_block.dart'; |
||||
import 'text_line.dart'; |
||||
|
||||
class QuillSimpleViewer extends StatefulWidget { |
||||
const QuillSimpleViewer({ |
||||
required this.controller, |
||||
this.customStyles, |
||||
this.truncate = false, |
||||
this.truncateScale, |
||||
this.truncateAlignment, |
||||
this.truncateHeight, |
||||
this.truncateWidth, |
||||
this.scrollBottomInset = 0, |
||||
this.padding = EdgeInsets.zero, |
||||
this.embedBuilder, |
||||
Key? key, |
||||
}) : assert(truncate || |
||||
((truncateScale == null) && |
||||
(truncateAlignment == null) && |
||||
(truncateHeight == null) && |
||||
(truncateWidth == null))), |
||||
super(key: key); |
||||
|
||||
final QuillController controller; |
||||
final DefaultStyles? customStyles; |
||||
final bool truncate; |
||||
final double? truncateScale; |
||||
final Alignment? truncateAlignment; |
||||
final double? truncateHeight; |
||||
final double? truncateWidth; |
||||
final double scrollBottomInset; |
||||
final EdgeInsetsGeometry padding; |
||||
final EmbedBuilder? embedBuilder; |
||||
|
||||
@override |
||||
_QuillSimpleViewerState createState() => _QuillSimpleViewerState(); |
||||
} |
||||
|
||||
class _QuillSimpleViewerState extends State<QuillSimpleViewer> |
||||
with SingleTickerProviderStateMixin { |
||||
late DefaultStyles _styles; |
||||
final LayerLink _toolbarLayerLink = LayerLink(); |
||||
final LayerLink _startHandleLayerLink = LayerLink(); |
||||
final LayerLink _endHandleLayerLink = LayerLink(); |
||||
late CursorCont _cursorCont; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
|
||||
_cursorCont = CursorCont( |
||||
show: ValueNotifier<bool>(false), |
||||
style: const CursorStyle( |
||||
color: Colors.black, |
||||
backgroundColor: Colors.grey, |
||||
width: 2, |
||||
radius: Radius.zero, |
||||
offset: Offset.zero, |
||||
), |
||||
tickerProvider: this, |
||||
); |
||||
} |
||||
|
||||
@override |
||||
void didChangeDependencies() { |
||||
super.didChangeDependencies(); |
||||
final parentStyles = QuillStyles.getStyles(context, true); |
||||
final defaultStyles = DefaultStyles.getInstance(context); |
||||
_styles = (parentStyles != null) |
||||
? defaultStyles.merge(parentStyles) |
||||
: defaultStyles; |
||||
|
||||
if (widget.customStyles != null) { |
||||
_styles = _styles.merge(widget.customStyles!); |
||||
} |
||||
} |
||||
|
||||
EmbedBuilder get embedBuilder => widget.embedBuilder ?? _defaultEmbedBuilder; |
||||
|
||||
Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) { |
||||
assert(!kIsWeb, 'Please provide EmbedBuilder for Web'); |
||||
switch (node.value.type) { |
||||
case 'image': |
||||
final imageUrl = _standardizeImageUrl(node.value.data); |
||||
return imageUrl.startsWith('http') |
||||
? Image.network(imageUrl) |
||||
: isBase64(imageUrl) |
||||
? Image.memory(base64.decode(imageUrl)) |
||||
: Image.file(io.File(imageUrl)); |
||||
default: |
||||
throw UnimplementedError( |
||||
'Embeddable type "${node.value.type}" is not supported by default embed ' |
||||
'builder of QuillEditor. You must pass your own builder function to ' |
||||
'embedBuilder property of QuillEditor or QuillField widgets.'); |
||||
} |
||||
} |
||||
|
||||
String _standardizeImageUrl(String url) { |
||||
if (url.contains('base64')) { |
||||
return url.split(',')[1]; |
||||
} |
||||
return url; |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final _doc = widget.controller.document; |
||||
// if (_doc.isEmpty() && |
||||
// !widget.focusNode.hasFocus && |
||||
// widget.placeholder != null) { |
||||
// _doc = Document.fromJson(jsonDecode( |
||||
// '[{"attributes":{"placeholder":true},"insert":"${widget.placeholder}\\n"}]')); |
||||
// } |
||||
|
||||
Widget child = CompositedTransformTarget( |
||||
link: _toolbarLayerLink, |
||||
child: Semantics( |
||||
child: _SimpleViewer( |
||||
document: _doc, |
||||
textDirection: _textDirection, |
||||
startHandleLayerLink: _startHandleLayerLink, |
||||
endHandleLayerLink: _endHandleLayerLink, |
||||
onSelectionChanged: _nullSelectionChanged, |
||||
scrollBottomInset: widget.scrollBottomInset, |
||||
padding: widget.padding, |
||||
children: _buildChildren(_doc, context), |
||||
), |
||||
), |
||||
); |
||||
|
||||
if (widget.truncate) { |
||||
if (widget.truncateScale != null) { |
||||
child = Container( |
||||
height: widget.truncateHeight, |
||||
child: Align( |
||||
heightFactor: widget.truncateScale, |
||||
widthFactor: widget.truncateScale, |
||||
alignment: widget.truncateAlignment ?? Alignment.topLeft, |
||||
child: Container( |
||||
width: widget.truncateWidth! / widget.truncateScale!, |
||||
child: SingleChildScrollView( |
||||
physics: const NeverScrollableScrollPhysics(), |
||||
child: Transform.scale( |
||||
scale: widget.truncateScale!, |
||||
alignment: |
||||
widget.truncateAlignment ?? Alignment.topLeft, |
||||
child: child))))); |
||||
} else { |
||||
child = Container( |
||||
height: widget.truncateHeight, |
||||
width: widget.truncateWidth, |
||||
child: SingleChildScrollView( |
||||
physics: const NeverScrollableScrollPhysics(), child: child)); |
||||
} |
||||
} |
||||
|
||||
return QuillStyles(data: _styles, child: child); |
||||
} |
||||
|
||||
List<Widget> _buildChildren(Document doc, BuildContext context) { |
||||
final result = <Widget>[]; |
||||
final indentLevelCounts = <int, int>{}; |
||||
for (final node in doc.root.children) { |
||||
if (node is Line) { |
||||
final editableTextLine = _getEditableTextLineFromNode(node, context); |
||||
result.add(editableTextLine); |
||||
} 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) |
||||
? const EdgeInsets.all(16) |
||||
: null, |
||||
embedBuilder, |
||||
_cursorCont, |
||||
indentLevelCounts); |
||||
result.add(editableTextBlock); |
||||
} else { |
||||
throw StateError('Unreachable.'); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
TextDirection get _textDirection { |
||||
final result = Directionality.of(context); |
||||
return result; |
||||
} |
||||
|
||||
EditableTextLine _getEditableTextLineFromNode( |
||||
Line node, BuildContext context) { |
||||
final textLine = TextLine( |
||||
line: node, |
||||
textDirection: _textDirection, |
||||
embedBuilder: embedBuilder, |
||||
styles: _styles, |
||||
); |
||||
final editableTextLine = EditableTextLine( |
||||
node, |
||||
null, |
||||
textLine, |
||||
0, |
||||
_getVerticalSpacingForLine(node, _styles), |
||||
_textDirection, |
||||
widget.controller.selection, |
||||
Colors.black, |
||||
//widget.selectionColor, |
||||
false, |
||||
//enableInteractiveSelection, |
||||
false, |
||||
//_hasFocus, |
||||
MediaQuery.of(context).devicePixelRatio, |
||||
_cursorCont); |
||||
return editableTextLine; |
||||
} |
||||
|
||||
Tuple2<double, double> _getVerticalSpacingForLine( |
||||
Line line, DefaultStyles? defaultStyles) { |
||||
final attrs = line.style.attributes; |
||||
if (attrs.containsKey(Attribute.header.key)) { |
||||
final int? level = attrs[Attribute.header.key]!.value; |
||||
switch (level) { |
||||
case 1: |
||||
return defaultStyles!.h1!.verticalSpacing; |
||||
case 2: |
||||
return defaultStyles!.h2!.verticalSpacing; |
||||
case 3: |
||||
return defaultStyles!.h3!.verticalSpacing; |
||||
default: |
||||
throw 'Invalid level $level'; |
||||
} |
||||
} |
||||
|
||||
return defaultStyles!.paragraph!.verticalSpacing; |
||||
} |
||||
|
||||
Tuple2<double, double> _getVerticalSpacingForBlock( |
||||
Block node, DefaultStyles? defaultStyles) { |
||||
final attrs = node.style.attributes; |
||||
if (attrs.containsKey(Attribute.blockQuote.key)) { |
||||
return defaultStyles!.quote!.verticalSpacing; |
||||
} else if (attrs.containsKey(Attribute.codeBlock.key)) { |
||||
return defaultStyles!.code!.verticalSpacing; |
||||
} else if (attrs.containsKey(Attribute.indent.key)) { |
||||
return defaultStyles!.indent!.verticalSpacing; |
||||
} |
||||
return defaultStyles!.lists!.verticalSpacing; |
||||
} |
||||
|
||||
void _nullSelectionChanged( |
||||
TextSelection selection, SelectionChangedCause cause) {} |
||||
} |
||||
|
||||
class _SimpleViewer extends MultiChildRenderObjectWidget { |
||||
_SimpleViewer({ |
||||
required List<Widget> children, |
||||
required this.document, |
||||
required this.textDirection, |
||||
required this.startHandleLayerLink, |
||||
required this.endHandleLayerLink, |
||||
required this.onSelectionChanged, |
||||
required this.scrollBottomInset, |
||||
this.padding = EdgeInsets.zero, |
||||
Key? key, |
||||
}) : super(key: key, children: children); |
||||
|
||||
final Document document; |
||||
final TextDirection textDirection; |
||||
final LayerLink startHandleLayerLink; |
||||
final LayerLink endHandleLayerLink; |
||||
final TextSelectionChangedHandler onSelectionChanged; |
||||
final double scrollBottomInset; |
||||
final EdgeInsetsGeometry padding; |
||||
|
||||
@override |
||||
RenderEditor createRenderObject(BuildContext context) { |
||||
return RenderEditor( |
||||
null, |
||||
textDirection, |
||||
scrollBottomInset, |
||||
padding, |
||||
document, |
||||
const TextSelection(baseOffset: 0, extentOffset: 0), |
||||
false, |
||||
// hasFocus, |
||||
onSelectionChanged, |
||||
startHandleLayerLink, |
||||
endHandleLayerLink, |
||||
const EdgeInsets.fromLTRB(4, 4, 4, 5), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
void updateRenderObject( |
||||
BuildContext context, covariant RenderEditor renderObject) { |
||||
renderObject |
||||
..document = document |
||||
..setContainer(document.root) |
||||
..textDirection = textDirection |
||||
..setStartHandleLayerLink(startHandleLayerLink) |
||||
..setEndHandleLayerLink(endHandleLayerLink) |
||||
..onSelectionChanged = onSelectionChanged |
||||
..setScrollBottomInset(scrollBottomInset) |
||||
..setPadding(padding); |
||||
} |
||||
} |
Loading…
Reference in new issue