|
@@ -0,0 +1,223 @@
|
|
|
|
+import 'package:example/plugin/debuggable_rich_text.dart';
|
|
|
|
+import 'package:flowy_editor/flowy_editor.dart';
|
|
|
|
+import 'package:flutter/foundation.dart';
|
|
|
|
+import 'package:flutter/gestures.dart';
|
|
|
|
+import 'package:flutter/material.dart';
|
|
|
|
+import 'package:flutter/rendering.dart';
|
|
|
|
+import 'package:url_launcher/url_launcher_string.dart';
|
|
|
|
+
|
|
|
|
+class SelectedTextNodeBuilder extends NodeWidgetBuilder {
|
|
|
|
+ SelectedTextNodeBuilder.create({
|
|
|
|
+ required super.node,
|
|
|
|
+ required super.editorState,
|
|
|
|
+ required super.key,
|
|
|
|
+ }) : super.create() {
|
|
|
|
+ nodeValidator = ((node) {
|
|
|
|
+ return node.type == 'text';
|
|
|
|
+ });
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ Widget build(BuildContext buildContext) {
|
|
|
|
+ return _SelectedTextNodeWidget(
|
|
|
|
+ key: key,
|
|
|
|
+ node: node,
|
|
|
|
+ editorState: editorState,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class _SelectedTextNodeWidget extends StatefulWidget {
|
|
|
|
+ final Node node;
|
|
|
|
+ final EditorState editorState;
|
|
|
|
+
|
|
|
|
+ const _SelectedTextNodeWidget({
|
|
|
|
+ Key? key,
|
|
|
|
+ required this.node,
|
|
|
|
+ required this.editorState,
|
|
|
|
+ }) : super(key: key);
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ State<_SelectedTextNodeWidget> createState() =>
|
|
|
|
+ _SelectedTextNodeWidgetState();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class _SelectedTextNodeWidgetState extends State<_SelectedTextNodeWidget>
|
|
|
|
+ with Selectable {
|
|
|
|
+ TextNode get node => widget.node as TextNode;
|
|
|
|
+ EditorState get editorState => widget.editorState;
|
|
|
|
+
|
|
|
|
+ final _textKey = GlobalKey();
|
|
|
|
+
|
|
|
|
+ RenderParagraph get _renderParagraph =>
|
|
|
|
+ _textKey.currentContext?.findRenderObject() as RenderParagraph;
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ List<Rect> getOverlayRectsInRange(Offset start, Offset end) {
|
|
|
|
+ // Returns select all if the start or end exceeds the size of the box
|
|
|
|
+ // TODO: don't need to compute everytime.
|
|
|
|
+ var rects = _computeSelectionRects(
|
|
|
|
+ TextSelection(baseOffset: 0, extentOffset: node.toRawString().length),
|
|
|
|
+ );
|
|
|
|
+
|
|
|
|
+ if (end.dy > start.dy) {
|
|
|
|
+ // downward
|
|
|
|
+ if (end.dy >= rects.last.bottom) {
|
|
|
|
+ return rects;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ // upward
|
|
|
|
+ if (end.dy <= rects.first.top) {
|
|
|
|
+ return rects;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ final selectionBaseOffset = _getTextPositionAtOffset(start).offset;
|
|
|
|
+ final selectionExtentOffset = _getTextPositionAtOffset(end).offset;
|
|
|
|
+ final textSelection = TextSelection(
|
|
|
|
+ baseOffset: selectionBaseOffset,
|
|
|
|
+ extentOffset: selectionExtentOffset,
|
|
|
|
+ );
|
|
|
|
+ return _computeSelectionRects(textSelection);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
+ Widget richText;
|
|
|
|
+ if (kDebugMode) {
|
|
|
|
+ richText = DebuggableRichText(text: node.toTextSpan(), textKey: _textKey);
|
|
|
|
+ } else {
|
|
|
|
+ richText = RichText(key: _textKey, text: node.toTextSpan());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return Column(
|
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
+ children: [
|
|
|
|
+ richText,
|
|
|
|
+ if (node.children.isNotEmpty)
|
|
|
|
+ ...node.children.map(
|
|
|
|
+ (e) => editorState.renderPlugins.buildWidget(
|
|
|
|
+ context: NodeWidgetContext(
|
|
|
|
+ buildContext: context,
|
|
|
|
+ node: e,
|
|
|
|
+ editorState: editorState,
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ ),
|
|
|
|
+ const SizedBox(
|
|
|
|
+ height: 5,
|
|
|
|
+ ),
|
|
|
|
+ ],
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ TextPosition _getTextPositionAtOffset(Offset offset) {
|
|
|
|
+ final textOffset = _renderParagraph.globalToLocal(offset);
|
|
|
|
+ return _renderParagraph.getPositionForOffset(textOffset);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ List<Rect> _computeSelectionRects(TextSelection selection) {
|
|
|
|
+ final textBoxes = _renderParagraph.getBoxesForSelection(selection);
|
|
|
|
+ return textBoxes
|
|
|
|
+ .map((box) =>
|
|
|
|
+ _renderParagraph.localToGlobal(box.toRect().topLeft) &
|
|
|
|
+ box.toRect().size)
|
|
|
|
+ .toList();
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+extension on TextNode {
|
|
|
|
+ TextSpan toTextSpan() => TextSpan(
|
|
|
|
+ children: delta.operations
|
|
|
|
+ .whereType<TextInsert>()
|
|
|
|
+ .map((op) => op.toTextSpan())
|
|
|
|
+ .toList());
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+extension on TextInsert {
|
|
|
|
+ TextSpan toTextSpan() {
|
|
|
|
+ FontWeight? fontWeight;
|
|
|
|
+ FontStyle? fontStyle;
|
|
|
|
+ TextDecoration? decoration;
|
|
|
|
+ GestureRecognizer? gestureRecognizer;
|
|
|
|
+ Color color = Colors.black;
|
|
|
|
+ Color highLightColor = Colors.transparent;
|
|
|
|
+ double fontSize = 16.0;
|
|
|
|
+ final attributes = this.attributes;
|
|
|
|
+ if (attributes?['bold'] == true) {
|
|
|
|
+ fontWeight = FontWeight.bold;
|
|
|
|
+ }
|
|
|
|
+ if (attributes?['italic'] == true) {
|
|
|
|
+ fontStyle = FontStyle.italic;
|
|
|
|
+ }
|
|
|
|
+ if (attributes?['underline'] == true) {
|
|
|
|
+ decoration = TextDecoration.underline;
|
|
|
|
+ }
|
|
|
|
+ if (attributes?['strikethrough'] == true) {
|
|
|
|
+ decoration = TextDecoration.lineThrough;
|
|
|
|
+ }
|
|
|
|
+ if (attributes?['highlight'] is String) {
|
|
|
|
+ highLightColor = Color(int.parse(attributes!['highlight']));
|
|
|
|
+ }
|
|
|
|
+ if (attributes?['href'] is String) {
|
|
|
|
+ color = const Color.fromARGB(255, 55, 120, 245);
|
|
|
|
+ decoration = TextDecoration.underline;
|
|
|
|
+ gestureRecognizer = TapGestureRecognizer()
|
|
|
|
+ ..onTap = () {
|
|
|
|
+ launchUrlString(attributes?['href']);
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+ final heading = attributes?['heading'] as String?;
|
|
|
|
+ if (heading != null) {
|
|
|
|
+ // TODO: make it better
|
|
|
|
+ if (heading == 'h1') {
|
|
|
|
+ fontSize = 30.0;
|
|
|
|
+ } else if (heading == 'h2') {
|
|
|
|
+ fontSize = 20.0;
|
|
|
|
+ }
|
|
|
|
+ fontWeight = FontWeight.bold;
|
|
|
|
+ }
|
|
|
|
+ return TextSpan(
|
|
|
|
+ text: content,
|
|
|
|
+ style: TextStyle(
|
|
|
|
+ fontWeight: fontWeight,
|
|
|
|
+ fontStyle: fontStyle,
|
|
|
|
+ decoration: decoration,
|
|
|
|
+ color: color,
|
|
|
|
+ fontSize: fontSize,
|
|
|
|
+ backgroundColor: highLightColor,
|
|
|
|
+ ),
|
|
|
|
+ recognizer: gestureRecognizer,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+class FlowyPainter extends CustomPainter {
|
|
|
|
+ final List<Rect> _rects;
|
|
|
|
+ final Paint _paint;
|
|
|
|
+
|
|
|
|
+ FlowyPainter({
|
|
|
|
+ Key? key,
|
|
|
|
+ required Color color,
|
|
|
|
+ required List<Rect> rects,
|
|
|
|
+ bool fill = false,
|
|
|
|
+ }) : _rects = rects,
|
|
|
|
+ _paint = Paint()..color = color {
|
|
|
|
+ _paint.style = fill ? PaintingStyle.fill : PaintingStyle.stroke;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ void paint(Canvas canvas, Size size) {
|
|
|
|
+ for (final rect in _rects) {
|
|
|
|
+ canvas.drawRect(
|
|
|
|
+ rect,
|
|
|
|
+ _paint,
|
|
|
|
+ );
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @override
|
|
|
|
+ bool shouldRepaint(covariant CustomPainter oldDelegate) {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+}
|