mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: duplicate selectable_text.dart from flutter and modify selection implement
This commit is contained in:
parent
30483e9d1e
commit
ce953d802a
@ -0,0 +1,758 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// An eyeballed value that moves the cursor slightly left of where it is
|
||||||
|
/// rendered for text on Android so its positioning more accurately matches the
|
||||||
|
/// native iOS text cursor positioning.
|
||||||
|
///
|
||||||
|
/// This value is in device pixels, not logical pixels as is typically used
|
||||||
|
/// throughout the codebase.
|
||||||
|
const int iOSHorizontalOffset = -2;
|
||||||
|
|
||||||
|
class _TextSpanEditingController extends TextEditingController {
|
||||||
|
_TextSpanEditingController({required TextSpan textSpan})
|
||||||
|
: assert(textSpan != null),
|
||||||
|
_textSpan = textSpan,
|
||||||
|
super(text: textSpan.toPlainText(includeSemanticsLabels: false));
|
||||||
|
|
||||||
|
final TextSpan _textSpan;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextSpan buildTextSpan(
|
||||||
|
{required BuildContext context,
|
||||||
|
TextStyle? style,
|
||||||
|
required bool withComposing}) {
|
||||||
|
// This does not care about composing.
|
||||||
|
return TextSpan(
|
||||||
|
style: style,
|
||||||
|
children: <TextSpan>[_textSpan],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set text(String? newText) {
|
||||||
|
// This should never be reached.
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectableTextSelectionGestureDetectorBuilder
|
||||||
|
extends TextSelectionGestureDetectorBuilder {
|
||||||
|
_SelectableTextSelectionGestureDetectorBuilder({
|
||||||
|
required _FlowySelectableTextState state,
|
||||||
|
}) : _state = state,
|
||||||
|
super(delegate: state);
|
||||||
|
|
||||||
|
final _FlowySelectableTextState _state;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onForcePressStart(ForcePressDetails details) {
|
||||||
|
super.onForcePressStart(details);
|
||||||
|
if (delegate.selectionEnabled && shouldShowSelectionToolbar) {
|
||||||
|
editableText.showToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onForcePressEnd(ForcePressDetails details) {
|
||||||
|
// Not required.
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
|
||||||
|
if (delegate.selectionEnabled) {
|
||||||
|
renderEditable.selectWordsInRange(
|
||||||
|
from: details.globalPosition - details.offsetFromOrigin,
|
||||||
|
to: details.globalPosition,
|
||||||
|
cause: SelectionChangedCause.longPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSingleTapUp(TapUpDetails details) {
|
||||||
|
editableText.hideToolbar();
|
||||||
|
if (delegate.selectionEnabled) {
|
||||||
|
switch (Theme.of(_state.context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
// renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||||
|
// break;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
renderEditable.selectPosition(cause: SelectionChangedCause.tap);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_state.widget.onTap?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onSingleLongTapStart(LongPressStartDetails details) {
|
||||||
|
if (delegate.selectionEnabled) {
|
||||||
|
renderEditable.selectWord(cause: SelectionChangedCause.longPress);
|
||||||
|
Feedback.forLongPress(_state.context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A run of selectable text with a single style.
|
||||||
|
///
|
||||||
|
/// The [FlowySelectableText] widget displays a string of text with a single style.
|
||||||
|
/// The string might break across multiple lines or might all be displayed on
|
||||||
|
/// the same line depending on the layout constraints.
|
||||||
|
///
|
||||||
|
/// {@youtube 560 315 https://www.youtube.com/watch?v=ZSU3ZXOs6hc}
|
||||||
|
///
|
||||||
|
/// The [style] argument is optional. When omitted, the text will use the style
|
||||||
|
/// from the closest enclosing [DefaultTextStyle]. If the given style's
|
||||||
|
/// [TextStyle.inherit] property is true (the default), the given style will
|
||||||
|
/// be merged with the closest enclosing [DefaultTextStyle]. This merging
|
||||||
|
/// behavior is useful, for example, to make the text bold while using the
|
||||||
|
/// default font family and size.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.material.textfield.wantKeepAlive}
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// const SelectableText(
|
||||||
|
/// 'Hello! How are you?',
|
||||||
|
/// textAlign: TextAlign.center,
|
||||||
|
/// style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// Using the [SelectableText.rich] constructor, the [FlowySelectableText] widget can
|
||||||
|
/// display a paragraph with differently styled [TextSpan]s. The sample
|
||||||
|
/// that follows displays "Hello beautiful world" with different styles
|
||||||
|
/// for each word.
|
||||||
|
///
|
||||||
|
/// {@tool snippet}
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// const SelectableText.rich(
|
||||||
|
/// TextSpan(
|
||||||
|
/// text: 'Hello', // default text style
|
||||||
|
/// children: <TextSpan>[
|
||||||
|
/// TextSpan(text: ' beautiful ', style: TextStyle(fontStyle: FontStyle.italic)),
|
||||||
|
/// TextSpan(text: 'world', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// ## Interactivity
|
||||||
|
///
|
||||||
|
/// To make [FlowySelectableText] react to touch events, use callback [onTap] to achieve
|
||||||
|
/// the desired behavior.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Text], which is the non selectable version of this widget.
|
||||||
|
/// * [TextField], which is the editable version of this widget.
|
||||||
|
class FlowySelectableText extends StatefulWidget {
|
||||||
|
/// Creates a selectable text widget.
|
||||||
|
///
|
||||||
|
/// If the [style] argument is null, the text will use the style from the
|
||||||
|
/// closest enclosing [DefaultTextStyle].
|
||||||
|
///
|
||||||
|
|
||||||
|
/// The [showCursor], [autofocus], [dragStartBehavior], [selectionHeightStyle],
|
||||||
|
/// [selectionWidthStyle] and [data] parameters must not be null. If specified,
|
||||||
|
/// the [maxLines] argument must be greater than zero.
|
||||||
|
const FlowySelectableText(
|
||||||
|
String this.data, {
|
||||||
|
Key? key,
|
||||||
|
this.focusNode,
|
||||||
|
this.style,
|
||||||
|
this.strutStyle,
|
||||||
|
this.textAlign,
|
||||||
|
this.textDirection,
|
||||||
|
this.textScaleFactor,
|
||||||
|
this.showCursor = false,
|
||||||
|
this.autofocus = false,
|
||||||
|
ToolbarOptions? toolbarOptions,
|
||||||
|
this.minLines,
|
||||||
|
this.maxLines,
|
||||||
|
this.cursorWidth = 2.0,
|
||||||
|
this.cursorHeight,
|
||||||
|
this.cursorRadius,
|
||||||
|
this.cursorColor,
|
||||||
|
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||||
|
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.enableInteractiveSelection = true,
|
||||||
|
this.selectionControls,
|
||||||
|
this.onTap,
|
||||||
|
this.scrollPhysics,
|
||||||
|
this.semanticsLabel,
|
||||||
|
this.textHeightBehavior,
|
||||||
|
this.textWidthBasis,
|
||||||
|
this.onSelectionChanged,
|
||||||
|
}) : assert(showCursor != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(dragStartBehavior != null),
|
||||||
|
assert(selectionHeightStyle != null),
|
||||||
|
assert(selectionWidthStyle != null),
|
||||||
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
"minLines can't be greater than maxLines",
|
||||||
|
),
|
||||||
|
assert(
|
||||||
|
data != null,
|
||||||
|
'A non-null String must be provided to a SelectableText widget.',
|
||||||
|
),
|
||||||
|
textSpan = null,
|
||||||
|
toolbarOptions = toolbarOptions ??
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// Creates a selectable text widget with a [TextSpan].
|
||||||
|
///
|
||||||
|
/// The [textSpan] parameter must not be null and only contain [TextSpan] in
|
||||||
|
/// [textSpan].children. Other type of [InlineSpan] is not allowed.
|
||||||
|
///
|
||||||
|
/// The [autofocus] and [dragStartBehavior] arguments must not be null.
|
||||||
|
const FlowySelectableText.rich(
|
||||||
|
TextSpan this.textSpan, {
|
||||||
|
Key? key,
|
||||||
|
this.focusNode,
|
||||||
|
this.style,
|
||||||
|
this.strutStyle,
|
||||||
|
this.textAlign,
|
||||||
|
this.textDirection,
|
||||||
|
this.textScaleFactor,
|
||||||
|
this.showCursor = false,
|
||||||
|
this.autofocus = false,
|
||||||
|
ToolbarOptions? toolbarOptions,
|
||||||
|
this.minLines,
|
||||||
|
this.maxLines,
|
||||||
|
this.cursorWidth = 2.0,
|
||||||
|
this.cursorHeight,
|
||||||
|
this.cursorRadius,
|
||||||
|
this.cursorColor,
|
||||||
|
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
||||||
|
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
||||||
|
this.dragStartBehavior = DragStartBehavior.start,
|
||||||
|
this.enableInteractiveSelection = true,
|
||||||
|
this.selectionControls,
|
||||||
|
this.onTap,
|
||||||
|
this.scrollPhysics,
|
||||||
|
this.semanticsLabel,
|
||||||
|
this.textHeightBehavior,
|
||||||
|
this.textWidthBasis,
|
||||||
|
this.onSelectionChanged,
|
||||||
|
}) : assert(showCursor != null),
|
||||||
|
assert(autofocus != null),
|
||||||
|
assert(dragStartBehavior != null),
|
||||||
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
"minLines can't be greater than maxLines",
|
||||||
|
),
|
||||||
|
assert(
|
||||||
|
textSpan != null,
|
||||||
|
'A non-null TextSpan must be provided to a SelectableText.rich widget.',
|
||||||
|
),
|
||||||
|
data = null,
|
||||||
|
toolbarOptions = toolbarOptions ??
|
||||||
|
const ToolbarOptions(
|
||||||
|
selectAll: true,
|
||||||
|
copy: true,
|
||||||
|
),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
/// The text to display.
|
||||||
|
///
|
||||||
|
/// This will be null if a [textSpan] is provided instead.
|
||||||
|
final String? data;
|
||||||
|
|
||||||
|
/// The text to display as a [TextSpan].
|
||||||
|
///
|
||||||
|
/// This will be null if [data] is provided instead.
|
||||||
|
final TextSpan? textSpan;
|
||||||
|
|
||||||
|
/// Defines the focus for this widget.
|
||||||
|
///
|
||||||
|
/// Text is only selectable when widget is focused.
|
||||||
|
///
|
||||||
|
/// The [focusNode] is a long-lived object that's typically managed by a
|
||||||
|
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
||||||
|
///
|
||||||
|
/// To give the focus to this widget, provide a [focusNode] and then
|
||||||
|
/// use the current [FocusScope] to request the focus:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// FocusScope.of(context).requestFocus(myFocusNode);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This happens automatically when the widget is tapped.
|
||||||
|
///
|
||||||
|
/// To be notified when the widget gains or loses the focus, add a listener
|
||||||
|
/// to the [focusNode]:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If null, this widget will create its own [FocusNode] with
|
||||||
|
/// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
|
||||||
|
/// to be skipped over during focus traversal.
|
||||||
|
final FocusNode? focusNode;
|
||||||
|
|
||||||
|
/// The style to use for the text.
|
||||||
|
///
|
||||||
|
/// If null, defaults [DefaultTextStyle] of context.
|
||||||
|
final TextStyle? style;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.strutStyle}
|
||||||
|
final StrutStyle? strutStyle;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.textAlign}
|
||||||
|
final TextAlign? textAlign;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.textDirection}
|
||||||
|
final TextDirection? textDirection;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.textScaleFactor}
|
||||||
|
final double? textScaleFactor;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.autofocus}
|
||||||
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.minLines}
|
||||||
|
final int? minLines;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.maxLines}
|
||||||
|
final int? maxLines;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.showCursor}
|
||||||
|
final bool showCursor;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorWidth}
|
||||||
|
final double cursorWidth;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorHeight}
|
||||||
|
final double? cursorHeight;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.cursorRadius}
|
||||||
|
final Radius? cursorRadius;
|
||||||
|
|
||||||
|
/// The color to use when painting the cursor.
|
||||||
|
///
|
||||||
|
/// Defaults to the theme's `cursorColor` when null.
|
||||||
|
final Color? cursorColor;
|
||||||
|
|
||||||
|
/// Controls how tall the selection highlight boxes are computed to be.
|
||||||
|
///
|
||||||
|
/// See [ui.BoxHeightStyle] for details on available styles.
|
||||||
|
final ui.BoxHeightStyle selectionHeightStyle;
|
||||||
|
|
||||||
|
/// Controls how wide the selection highlight boxes are computed to be.
|
||||||
|
///
|
||||||
|
/// See [ui.BoxWidthStyle] for details on available styles.
|
||||||
|
final ui.BoxWidthStyle selectionWidthStyle;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
||||||
|
final bool enableInteractiveSelection;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.selectionControls}
|
||||||
|
final TextSelectionControls? selectionControls;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
||||||
|
final DragStartBehavior dragStartBehavior;
|
||||||
|
|
||||||
|
/// Configuration of toolbar options.
|
||||||
|
///
|
||||||
|
/// Paste and cut will be disabled regardless.
|
||||||
|
///
|
||||||
|
/// If not set, select all and copy will be enabled by default.
|
||||||
|
final ToolbarOptions toolbarOptions;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
||||||
|
bool get selectionEnabled => enableInteractiveSelection;
|
||||||
|
|
||||||
|
/// Called when the user taps on this selectable text.
|
||||||
|
///
|
||||||
|
/// The selectable text builds a [GestureDetector] to handle input events like tap,
|
||||||
|
/// to trigger focus requests, to move the caret, adjust the selection, etc.
|
||||||
|
/// Handling some of those events by wrapping the selectable text with a competing
|
||||||
|
/// GestureDetector is problematic.
|
||||||
|
///
|
||||||
|
/// To unconditionally handle taps, without interfering with the selectable text's
|
||||||
|
/// internal gesture detector, provide this callback.
|
||||||
|
///
|
||||||
|
/// To be notified when the text field gains or loses the focus, provide a
|
||||||
|
/// [focusNode] and add a listener to that.
|
||||||
|
///
|
||||||
|
/// To listen to arbitrary pointer events without competing with the
|
||||||
|
/// selectable text's internal gesture detector, use a [Listener].
|
||||||
|
final GestureTapCallback? onTap;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
||||||
|
final ScrollPhysics? scrollPhysics;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.Text.semanticsLabel}
|
||||||
|
final String? semanticsLabel;
|
||||||
|
|
||||||
|
/// {@macro dart.ui.textHeightBehavior}
|
||||||
|
final TextHeightBehavior? textHeightBehavior;
|
||||||
|
|
||||||
|
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
||||||
|
final TextWidthBasis? textWidthBasis;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.onSelectionChanged}
|
||||||
|
final SelectionChangedCallback? onSelectionChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FlowySelectableText> createState() => _FlowySelectableTextState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
.add(DiagnosticsProperty<String>('data', data, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<String>('semanticsLabel', semanticsLabel,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('showCursor', showCursor,
|
||||||
|
defaultValue: false));
|
||||||
|
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||||
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
|
||||||
|
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(
|
||||||
|
DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
|
||||||
|
properties
|
||||||
|
.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
|
||||||
|
properties
|
||||||
|
.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(FlagProperty('selectionEnabled',
|
||||||
|
value: selectionEnabled,
|
||||||
|
defaultValue: true,
|
||||||
|
ifFalse: 'selection disabled'));
|
||||||
|
properties.add(DiagnosticsProperty<TextSelectionControls>(
|
||||||
|
'selectionControls', selectionControls,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<ScrollPhysics>(
|
||||||
|
'scrollPhysics', scrollPhysics,
|
||||||
|
defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<TextHeightBehavior>(
|
||||||
|
'textHeightBehavior', textHeightBehavior,
|
||||||
|
defaultValue: null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FlowySelectableTextState extends State<FlowySelectableText>
|
||||||
|
implements TextSelectionGestureDetectorBuilderDelegate {
|
||||||
|
EditableTextState? get _editableText => editableTextKey.currentState;
|
||||||
|
|
||||||
|
late _TextSpanEditingController _controller;
|
||||||
|
|
||||||
|
FocusNode? _focusNode;
|
||||||
|
FocusNode get _effectiveFocusNode =>
|
||||||
|
widget.focusNode ?? (_focusNode ??= FocusNode(skipTraversal: true));
|
||||||
|
|
||||||
|
bool _showSelectionHandles = false;
|
||||||
|
|
||||||
|
late _SelectableTextSelectionGestureDetectorBuilder
|
||||||
|
_selectionGestureDetectorBuilder;
|
||||||
|
|
||||||
|
// API for TextSelectionGestureDetectorBuilderDelegate.
|
||||||
|
@override
|
||||||
|
late bool forcePressEnabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final GlobalKey<EditableTextState> editableTextKey =
|
||||||
|
GlobalKey<EditableTextState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get selectionEnabled => widget.selectionEnabled;
|
||||||
|
// End of API for TextSelectionGestureDetectorBuilderDelegate.
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_selectionGestureDetectorBuilder =
|
||||||
|
_SelectableTextSelectionGestureDetectorBuilder(state: this);
|
||||||
|
_controller = _TextSpanEditingController(
|
||||||
|
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
|
||||||
|
);
|
||||||
|
_controller.addListener(_onControllerChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(FlowySelectableText oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.data != oldWidget.data ||
|
||||||
|
widget.textSpan != oldWidget.textSpan) {
|
||||||
|
_controller.removeListener(_onControllerChanged);
|
||||||
|
_controller = _TextSpanEditingController(
|
||||||
|
textSpan: widget.textSpan ?? TextSpan(text: widget.data),
|
||||||
|
);
|
||||||
|
_controller.addListener(_onControllerChanged);
|
||||||
|
}
|
||||||
|
if (_effectiveFocusNode.hasFocus && _controller.selection.isCollapsed) {
|
||||||
|
_showSelectionHandles = false;
|
||||||
|
} else {
|
||||||
|
_showSelectionHandles = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_focusNode?.dispose();
|
||||||
|
_controller.removeListener(_onControllerChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onControllerChanged() {
|
||||||
|
final bool showSelectionHandles =
|
||||||
|
!_effectiveFocusNode.hasFocus || !_controller.selection.isCollapsed;
|
||||||
|
if (showSelectionHandles == _showSelectionHandles) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_showSelectionHandles = showSelectionHandles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TextSelection? _lastSeenTextSelection;
|
||||||
|
|
||||||
|
void _handleSelectionChanged(
|
||||||
|
TextSelection selection, SelectionChangedCause? cause) {
|
||||||
|
final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause);
|
||||||
|
if (willShowSelectionHandles != _showSelectionHandles) {
|
||||||
|
setState(() {
|
||||||
|
_showSelectionHandles = willShowSelectionHandles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// TODO(chunhtai): The selection may be the same. We should remove this
|
||||||
|
// check once this is fixed https://github.com/flutter/flutter/issues/76349.
|
||||||
|
if (widget.onSelectionChanged != null &&
|
||||||
|
_lastSeenTextSelection != selection) {
|
||||||
|
widget.onSelectionChanged!(selection, cause);
|
||||||
|
}
|
||||||
|
_lastSeenTextSelection = selection;
|
||||||
|
|
||||||
|
switch (Theme.of(context).platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
if (cause == SelectionChangedCause.longPress) {
|
||||||
|
_editableText?.bringIntoView(selection.base);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
// Do nothing.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle the toolbar when a selection handle is tapped.
|
||||||
|
void _handleSelectionHandleTapped() {
|
||||||
|
if (_controller.selection.isCollapsed) {
|
||||||
|
_editableText!.toggleToolbar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldShowSelectionHandles(SelectionChangedCause? cause) {
|
||||||
|
// When the text field is activated by something that doesn't trigger the
|
||||||
|
// selection overlay, we shouldn't show the handles either.
|
||||||
|
if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_controller.selection.isCollapsed) return false;
|
||||||
|
|
||||||
|
if (cause == SelectionChangedCause.keyboard) return false;
|
||||||
|
|
||||||
|
if (cause == SelectionChangedCause.longPress) return true;
|
||||||
|
|
||||||
|
if (_controller.text.isNotEmpty) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// TODO(garyq): Assert to block WidgetSpans from being used here are removed,
|
||||||
|
// but we still do not yet have nice handling of things like carets, clipboard,
|
||||||
|
// and other features. We should add proper support. Currently, caret handling
|
||||||
|
// is blocked on SkParagraph switch and https://github.com/flutter/engine/pull/27010
|
||||||
|
// should be landed in SkParagraph after the switch is complete.
|
||||||
|
assert(debugCheckHasMediaQuery(context));
|
||||||
|
assert(debugCheckHasDirectionality(context));
|
||||||
|
assert(
|
||||||
|
!(widget.style != null &&
|
||||||
|
widget.style!.inherit == false &&
|
||||||
|
(widget.style!.fontSize == null ||
|
||||||
|
widget.style!.textBaseline == null)),
|
||||||
|
'inherit false style must supply fontSize and textBaseline',
|
||||||
|
);
|
||||||
|
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
|
final TextSelectionThemeData selectionTheme =
|
||||||
|
TextSelectionTheme.of(context);
|
||||||
|
final FocusNode focusNode = _effectiveFocusNode;
|
||||||
|
|
||||||
|
TextSelectionControls? textSelectionControls = widget.selectionControls;
|
||||||
|
final bool paintCursorAboveText;
|
||||||
|
final bool cursorOpacityAnimates;
|
||||||
|
Offset? cursorOffset;
|
||||||
|
Color? cursorColor = widget.cursorColor;
|
||||||
|
final Color selectionColor;
|
||||||
|
Radius? cursorRadius = widget.cursorRadius;
|
||||||
|
|
||||||
|
switch (theme.platform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
|
forcePressEnabled = true;
|
||||||
|
textSelectionControls ??= cupertinoTextSelectionControls;
|
||||||
|
paintCursorAboveText = true;
|
||||||
|
cursorOpacityAnimates = true;
|
||||||
|
cursorColor ??=
|
||||||
|
selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
|
selectionColor = selectionTheme.selectionColor ??
|
||||||
|
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||||
|
cursorRadius ??= const Radius.circular(2.0);
|
||||||
|
cursorOffset = Offset(
|
||||||
|
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
|
||||||
|
forcePressEnabled = false;
|
||||||
|
textSelectionControls ??= cupertinoDesktopTextSelectionControls;
|
||||||
|
paintCursorAboveText = true;
|
||||||
|
cursorOpacityAnimates = true;
|
||||||
|
cursorColor ??=
|
||||||
|
selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
|
||||||
|
selectionColor = selectionTheme.selectionColor ??
|
||||||
|
cupertinoTheme.primaryColor.withOpacity(0.40);
|
||||||
|
cursorRadius ??= const Radius.circular(2.0);
|
||||||
|
cursorOffset = Offset(
|
||||||
|
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
forcePressEnabled = false;
|
||||||
|
textSelectionControls ??= materialTextSelectionControls;
|
||||||
|
paintCursorAboveText = false;
|
||||||
|
cursorOpacityAnimates = false;
|
||||||
|
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
|
||||||
|
selectionColor = selectionTheme.selectionColor ??
|
||||||
|
theme.colorScheme.primary.withOpacity(0.40);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
forcePressEnabled = false;
|
||||||
|
textSelectionControls ??= desktopTextSelectionControls;
|
||||||
|
paintCursorAboveText = false;
|
||||||
|
cursorOpacityAnimates = false;
|
||||||
|
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
|
||||||
|
selectionColor = selectionTheme.selectionColor ??
|
||||||
|
theme.colorScheme.primary.withOpacity(0.40);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
|
||||||
|
TextStyle? effectiveTextStyle = widget.style;
|
||||||
|
if (effectiveTextStyle == null || effectiveTextStyle.inherit)
|
||||||
|
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
|
||||||
|
if (MediaQuery.boldTextOverride(context))
|
||||||
|
effectiveTextStyle = effectiveTextStyle
|
||||||
|
.merge(const TextStyle(fontWeight: FontWeight.bold));
|
||||||
|
final Widget child = RepaintBoundary(
|
||||||
|
child: EditableText(
|
||||||
|
key: editableTextKey,
|
||||||
|
style: effectiveTextStyle,
|
||||||
|
readOnly: true,
|
||||||
|
textWidthBasis:
|
||||||
|
widget.textWidthBasis ?? defaultTextStyle.textWidthBasis,
|
||||||
|
textHeightBehavior:
|
||||||
|
widget.textHeightBehavior ?? defaultTextStyle.textHeightBehavior,
|
||||||
|
showSelectionHandles: _showSelectionHandles,
|
||||||
|
showCursor: widget.showCursor,
|
||||||
|
controller: _controller,
|
||||||
|
focusNode: focusNode,
|
||||||
|
strutStyle: widget.strutStyle ?? const StrutStyle(),
|
||||||
|
textAlign:
|
||||||
|
widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
|
||||||
|
textDirection: widget.textDirection,
|
||||||
|
textScaleFactor: widget.textScaleFactor,
|
||||||
|
autofocus: widget.autofocus,
|
||||||
|
forceLine: false,
|
||||||
|
toolbarOptions: widget.toolbarOptions,
|
||||||
|
minLines: widget.minLines,
|
||||||
|
maxLines: widget.maxLines ?? defaultTextStyle.maxLines,
|
||||||
|
selectionColor: selectionColor,
|
||||||
|
selectionControls:
|
||||||
|
widget.selectionEnabled ? textSelectionControls : null,
|
||||||
|
onSelectionChanged: _handleSelectionChanged,
|
||||||
|
onSelectionHandleTapped: _handleSelectionHandleTapped,
|
||||||
|
rendererIgnoresPointer: true,
|
||||||
|
cursorWidth: widget.cursorWidth,
|
||||||
|
cursorHeight: widget.cursorHeight,
|
||||||
|
cursorRadius: cursorRadius,
|
||||||
|
cursorColor: cursorColor,
|
||||||
|
selectionHeightStyle: widget.selectionHeightStyle,
|
||||||
|
selectionWidthStyle: widget.selectionWidthStyle,
|
||||||
|
cursorOpacityAnimates: cursorOpacityAnimates,
|
||||||
|
cursorOffset: cursorOffset,
|
||||||
|
paintCursorAboveText: paintCursorAboveText,
|
||||||
|
backgroundCursorColor: CupertinoColors.inactiveGray,
|
||||||
|
enableInteractiveSelection: widget.enableInteractiveSelection,
|
||||||
|
dragStartBehavior: widget.dragStartBehavior,
|
||||||
|
scrollPhysics: widget.scrollPhysics,
|
||||||
|
autofillHints: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
label: widget.semanticsLabel,
|
||||||
|
excludeSemantics: widget.semanticsLabel != null,
|
||||||
|
onLongPress: () {
|
||||||
|
_effectiveFocusNode.requestFocus();
|
||||||
|
},
|
||||||
|
child: _selectionGestureDetectorBuilder.buildGestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
import 'flowy_selectable_text.dart';
|
||||||
|
|
||||||
class TextNodeBuilder extends NodeWidgetBuilder {
|
class TextNodeBuilder extends NodeWidgetBuilder {
|
||||||
TextNodeBuilder.create({
|
TextNodeBuilder.create({
|
||||||
@ -37,23 +38,82 @@ class _TextNodeWidget extends StatefulWidget {
|
|||||||
State<_TextNodeWidget> createState() => __TextNodeWidgetState();
|
State<_TextNodeWidget> createState() => __TextNodeWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _textContentOfDelta(Delta delta) {
|
|
||||||
return delta.operations.fold("", (previousValue, element) {
|
|
||||||
if (element is TextInsert) {
|
|
||||||
return previousValue + element.content;
|
|
||||||
}
|
|
||||||
return previousValue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
class __TextNodeWidgetState extends State<_TextNodeWidget>
|
class __TextNodeWidgetState extends State<_TextNodeWidget>
|
||||||
implements DeltaTextInputClient {
|
implements DeltaTextInputClient {
|
||||||
final _focusNode = FocusNode(debugLabel: "input");
|
|
||||||
TextNode get node => widget.node as TextNode;
|
TextNode get node => widget.node as TextNode;
|
||||||
EditorState get editorState => widget.editorState;
|
EditorState get editorState => widget.editorState;
|
||||||
|
|
||||||
TextInputConnection? _textInputConnection;
|
TextInputConnection? _textInputConnection;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
FlowySelectableText.rich(
|
||||||
|
node.toTextSpan(),
|
||||||
|
showCursor: true,
|
||||||
|
enableInteractiveSelection: true,
|
||||||
|
onSelectionChanged: _onSelectionChanged,
|
||||||
|
// autofocus: true,
|
||||||
|
focusNode: FocusNode(
|
||||||
|
onKey: _onKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (node.children.isNotEmpty)
|
||||||
|
...node.children.map(
|
||||||
|
(e) => editorState.renderPlugins.buildWidget(
|
||||||
|
context: NodeWidgetContext(
|
||||||
|
buildContext: context,
|
||||||
|
node: e,
|
||||||
|
editorState: editorState,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult _onKey(FocusNode focusNode, RawKeyEvent event) {
|
||||||
|
if (event is RawKeyDownEvent) {
|
||||||
|
final sel = _globalSelectionToLocal(node, editorState.cursorSelection);
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||||
|
_backDeleteTextAtSelection(sel);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
} else if (event.logicalKey == LogicalKeyboardKey.delete) {
|
||||||
|
_forwardDeleteTextAtSelection(sel);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSelectionChanged(
|
||||||
|
TextSelection selection, SelectionChangedCause? cause) {
|
||||||
|
_textInputConnection?.close();
|
||||||
|
_textInputConnection = TextInput.attach(
|
||||||
|
this,
|
||||||
|
const TextInputConfiguration(
|
||||||
|
enableDeltaModel: true,
|
||||||
|
inputType: TextInputType.multiline,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
debugPrint('selection: $selection');
|
||||||
|
editorState.cursorSelection = _localSelectionToGlobal(node, selection);
|
||||||
|
_textInputConnection
|
||||||
|
?..show()
|
||||||
|
..setEditingState(
|
||||||
|
TextEditingValue(
|
||||||
|
text: node.toRawString(),
|
||||||
|
selection: selection,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_backDeleteTextAtSelection(TextSelection? sel) {
|
_backDeleteTextAtSelection(TextSelection? sel) {
|
||||||
if (sel == null) {
|
if (sel == null) {
|
||||||
return;
|
return;
|
||||||
@ -94,78 +154,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
|
|||||||
|
|
||||||
_setEditingStateFromGlobal() {
|
_setEditingStateFromGlobal() {
|
||||||
_textInputConnection?.setEditingState(TextEditingValue(
|
_textInputConnection?.setEditingState(TextEditingValue(
|
||||||
text: node.toString(),
|
text: node.toRawString(),
|
||||||
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
|
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
|
||||||
const TextSelection.collapsed(offset: 0)));
|
const TextSelection.collapsed(offset: 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
KeyboardListener(
|
|
||||||
focusNode: _focusNode,
|
|
||||||
onKeyEvent: ((value) {
|
|
||||||
if (value is KeyDownEvent || value is KeyRepeatEvent) {
|
|
||||||
final sel =
|
|
||||||
_globalSelectionToLocal(node, editorState.cursorSelection);
|
|
||||||
if (value.logicalKey.keyLabel == "Backspace") {
|
|
||||||
_backDeleteTextAtSelection(sel);
|
|
||||||
} else if (value.logicalKey.keyLabel == "Delete") {
|
|
||||||
_forwardDeleteTextAtSelection(sel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
child: SelectableText.rich(
|
|
||||||
showCursor: true,
|
|
||||||
TextSpan(
|
|
||||||
children: node.toTextSpans(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
},
|
|
||||||
onSelectionChanged: ((selection, cause) {
|
|
||||||
_textInputConnection?.close();
|
|
||||||
_textInputConnection = TextInput.attach(
|
|
||||||
this,
|
|
||||||
const TextInputConfiguration(
|
|
||||||
enableDeltaModel: true,
|
|
||||||
inputType: TextInputType.multiline,
|
|
||||||
textCapitalization: TextCapitalization.sentences,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
debugPrint('selection: $selection');
|
|
||||||
editorState.cursorSelection =
|
|
||||||
_localSelectionToGlobal(node, selection);
|
|
||||||
_textInputConnection
|
|
||||||
?..show()
|
|
||||||
..setEditingState(
|
|
||||||
TextEditingValue(
|
|
||||||
text: node.toString(),
|
|
||||||
selection: selection,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (node.children.isNotEmpty)
|
|
||||||
...node.children.map(
|
|
||||||
(e) => editorState.renderPlugins.buildWidget(
|
|
||||||
context: NodeWidgetContext(
|
|
||||||
buildContext: context,
|
|
||||||
node: e,
|
|
||||||
editorState: editorState,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void connectionClosed() {
|
void connectionClosed() {
|
||||||
// TODO: implement connectionClosed
|
// TODO: implement connectionClosed
|
||||||
@ -178,7 +171,7 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
|
|||||||
@override
|
@override
|
||||||
// TODO: implement currentTextEditingValue
|
// TODO: implement currentTextEditingValue
|
||||||
TextEditingValue? get currentTextEditingValue => TextEditingValue(
|
TextEditingValue? get currentTextEditingValue => TextEditingValue(
|
||||||
text: node.toString(),
|
text: node.toRawString(),
|
||||||
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
|
selection: _globalSelectionToLocal(node, editorState.cursorSelection) ??
|
||||||
const TextSelection.collapsed(offset: 0));
|
const TextSelection.collapsed(offset: 0));
|
||||||
|
|
||||||
@ -241,10 +234,11 @@ class __TextNodeWidgetState extends State<_TextNodeWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension on TextNode {
|
extension on TextNode {
|
||||||
List<TextSpan> toTextSpans() => delta.operations
|
TextSpan toTextSpan() => TextSpan(
|
||||||
.whereType<TextInsert>()
|
children: delta.operations
|
||||||
.map((op) => op.toTextSpan())
|
.whereType<TextInsert>()
|
||||||
.toList();
|
.map((op) => op.toTextSpan())
|
||||||
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on TextInsert {
|
extension on TextInsert {
|
||||||
|
@ -184,10 +184,8 @@ class TextNode extends Node {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
String toString() {
|
String toRawString() => _delta.operations
|
||||||
_delta.operations
|
.whereType<TextInsert>()
|
||||||
.whereType<TextInsert>()
|
.map((op) => op.content)
|
||||||
.map((op) => op.content)
|
.toString();
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user