fix: paste issue from ChatGPT (#1146)

* chore: remove diff issue

* fix: deepcopy the children node when delete node

* fix: auto scroller issue

* fix: paste content from chatgpt
This commit is contained in:
Lucas 2025-10-22 13:59:26 +08:00 • committed by GitHub
commit 4c1d2eca35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 312 additions and 226 deletions

View file

@ -408,7 +408,8 @@ class Delta extends Iterable<TextOperation> {
if (op is TextInsert) { if (op is TextInsert) {
return op.text; return op.text;
} }
throw Exception('diff() called $e'); assert(false, 'diff() called with non-insert operations');
return '';
}).join(); }).join();
return text; return text;
}).toList(); }).toList();

View file

@ -50,7 +50,7 @@ CommandShortcutEventHandler _convertToParagraphCommandHandler = (editorState) {
ParagraphBlockKeys.delta: delta.toJson(), ParagraphBlockKeys.delta: delta.toJson(),
}, },
textDirection: textDirection, textDirection: textDirection,
children: node.children, children: node.children.map((e) => e.deepCopy()).toList(),
), ),
deepCopy: true, deepCopy: true,
) )

View file

@ -34,7 +34,6 @@ class AutoScroller extends EdgeDraggingAutoScroller
if (direction != null && direction == AxisDirection.up) { if (direction != null && direction == AxisDirection.up) {
return startAutoScrollIfNecessary( return startAutoScrollIfNecessary(
offset & Size(1, edgeOffset), offset & Size(1, edgeOffset),
duration: duration,
); );
} }
@ -47,7 +46,6 @@ class AutoScroller extends EdgeDraggingAutoScroller
startAutoScrollIfNecessary( startAutoScrollIfNecessary(
dragTarget, dragTarget,
duration: duration,
); );
} }

View file

@ -13,25 +13,24 @@ import 'package:flutter/material.dart';
export 'package:flutter/physics.dart' show Tolerance; export 'package:flutter/physics.dart' show Tolerance;
/// Describes the aspects of a Scrollable widget to inform inherited widgets /// Describes the aspects of a Scrollable widget to inform inherited widgets
/// like [ScrollBehavior] for decorating. /// like [ScrollBehavior] for decorating or enumerate the properties of combined
// TODO(Piinks): Fix doc with 2DScrollable change. /// Scrollables, such as [TwoDimensionalScrollable].
// or enumerate the properties of combined
// Scrollables, such as [TwoDimensionalScrollable].
/// ///
/// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require /// Decorations like [GlowingOverscrollIndicator]s and [Scrollbar]s require
/// information about the Scrollable in order to be initialized. /// information about the Scrollable in order to be initialized.
@immutable @immutable
class ScrollableDetails { class ScrollableDetails {
/// Creates a set of details describing the [Scrollable]. The [direction] /// Creates a set of details describing the [Scrollable].
/// cannot be null.
const ScrollableDetails({ const ScrollableDetails({
required this.direction, required this.direction,
this.controller, this.controller,
this.physics, this.physics,
@Deprecated('Migrate to decorationClipBehavior. ' @Deprecated(
'This property was deprecated so that its application is clearer. This clip ' 'Migrate to decorationClipBehavior. '
'applies to decorators, and does not directly clip a scroll view. ' 'This property was deprecated so that its application is clearer. This clip '
'This feature was deprecated after v3.9.0-1.0.pre.') 'applies to decorators, and does not directly clip a scroll view. '
'This feature was deprecated after v3.9.0-1.0.pre.',
)
Clip? clipBehavior, Clip? clipBehavior,
Clip? decorationClipBehavior, Clip? decorationClipBehavior,
}) : decorationClipBehavior = clipBehavior ?? decorationClipBehavior; }) : decorationClipBehavior = clipBehavior ?? decorationClipBehavior;
@ -74,10 +73,12 @@ class ScrollableDetails {
final Clip? decorationClipBehavior; final Clip? decorationClipBehavior;
/// Deprecated getter for [decorationClipBehavior]. /// Deprecated getter for [decorationClipBehavior].
@Deprecated('Migrate to decorationClipBehavior. ' @Deprecated(
'This property was deprecated so that its application is clearer. This clip ' 'Migrate to decorationClipBehavior. '
'applies to decorators, and does not directly clip a scroll view. ' 'This property was deprecated so that its application is clearer. This clip '
'This feature was deprecated after v3.9.0-1.0.pre.') 'applies to decorators, and does not directly clip a scroll view. '
'This feature was deprecated after v3.9.0-1.0.pre.',
)
Clip? get clipBehavior => decorationClipBehavior; Clip? get clipBehavior => decorationClipBehavior;
/// Copy the current [ScrollableDetails] with the given values replacing the /// Copy the current [ScrollableDetails] with the given values replacing the
@ -115,12 +116,8 @@ class ScrollableDetails {
} }
@override @override
int get hashCode => Object.hash( int get hashCode =>
direction, Object.hash(direction, controller, physics, decorationClipBehavior);
controller,
physics,
decorationClipBehavior,
);
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@ -143,18 +140,15 @@ class ScrollableDetails {
/// ///
/// The scroll velocity is controlled by the [velocityScalar]: /// The scroll velocity is controlled by the [velocityScalar]:
/// ///
/// velocity = &lt;distance of overscroll&gt; * [velocityScalar]. /// velocity = (distance of overscroll) * [velocityScalar].
class EdgeDraggingAutoScroller { class EdgeDraggingAutoScroller {
/// Creates a auto scroller that scrolls the [scrollable]. /// Creates a auto scroller that scrolls the [scrollable].
EdgeDraggingAutoScroller( EdgeDraggingAutoScroller(
this.scrollable, { this.scrollable, {
this.onScrollViewScrolled, this.onScrollViewScrolled,
this.velocityScalar = _kDefaultAutoScrollVelocityScalar, required this.velocityScalar,
}); });
// An eyeballed value for a smooth scrolling experience.
static const double _kDefaultAutoScrollVelocityScalar = 7;
/// The [Scrollable] this auto scroller is scrolling. /// The [Scrollable] this auto scroller is scrolling.
final ScrollableState scrollable; final ScrollableState scrollable;
@ -165,10 +159,12 @@ class EdgeDraggingAutoScroller {
/// in between each scroll. /// in between each scroll.
final VoidCallback? onScrollViewScrolled; final VoidCallback? onScrollViewScrolled;
/// {@template flutter.widgets.EdgeDraggingAutoScroller.velocityScalar}
/// The velocity scalar per pixel over scroll. /// The velocity scalar per pixel over scroll.
/// ///
/// It represents how the velocity scale with the over scroll distance. The /// It represents how the velocity scale with the over scroll distance. The
/// auto-scroll velocity = &lt;distance of overscroll&gt; * velocityScalar. /// auto-scroll velocity = (distance of overscroll) * velocityScalar.
/// {@endtemplate}
final double velocityScalar; final double velocityScalar;
late Rect _dragTargetRelatedToScrollOrigin; late Rect _dragTargetRelatedToScrollOrigin;
@ -178,21 +174,17 @@ class EdgeDraggingAutoScroller {
bool _scrolling = false; bool _scrolling = false;
double _offsetExtent(Offset offset, Axis scrollDirection) { double _offsetExtent(Offset offset, Axis scrollDirection) {
switch (scrollDirection) { return switch (scrollDirection) {
case Axis.horizontal: Axis.horizontal => offset.dx,
return offset.dx; Axis.vertical => offset.dy,
case Axis.vertical: };
return offset.dy;
}
} }
double _sizeExtent(Size size, Axis scrollDirection) { double _sizeExtent(Size size, Axis scrollDirection) {
switch (scrollDirection) { return switch (scrollDirection) {
case Axis.horizontal: Axis.horizontal => size.width,
return size.width; Axis.vertical => size.height,
case Axis.vertical: };
return size.height;
}
} }
AxisDirection get _axisDirection => scrollable.axisDirection; AxisDirection get _axisDirection => scrollable.axisDirection;
@ -205,7 +197,7 @@ class EdgeDraggingAutoScroller {
/// ///
/// If the scrollable is already scrolling, calling this method updates the /// If the scrollable is already scrolling, calling this method updates the
/// previous dragTarget to the new value and continues scrolling if necessary. /// previous dragTarget to the new value and continues scrolling if necessary.
void startAutoScrollIfNecessary(Rect dragTarget, {Duration? duration}) { void startAutoScrollIfNecessary(Rect dragTarget) {
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin; final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
_dragTargetRelatedToScrollOrigin = _dragTargetRelatedToScrollOrigin =
dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy); dragTarget.translate(deltaToOrigin.dx, deltaToOrigin.dy);
@ -214,7 +206,7 @@ class EdgeDraggingAutoScroller {
return; return;
} }
assert(!_scrolling); assert(!_scrolling);
_scroll(duration: duration); _scroll();
} }
/// Stop any ongoing auto scrolling. /// Stop any ongoing auto scrolling.
@ -222,100 +214,115 @@ class EdgeDraggingAutoScroller {
_scrolling = false; _scrolling = false;
} }
Future<void> _scroll({Duration? duration}) async { Future<void> _scroll() async {
final RenderBox scrollRenderBox = try {
scrollable.context.findRenderObject()! as RenderBox; final RenderBox scrollRenderBox =
final Rect globalRect = MatrixUtils.transformRect( scrollable.context.findRenderObject()! as RenderBox;
scrollRenderBox.getTransformTo(null), final Matrix4 transform = scrollRenderBox.getTransformTo(null);
Rect.fromLTWH( final Rect globalRect = MatrixUtils.transformRect(
0, transform,
0, Rect.fromLTWH(
scrollRenderBox.size.width, 0,
scrollRenderBox.size.height, 0,
), scrollRenderBox.size.width,
); scrollRenderBox.size.height,
),
_scrolling = true; );
double? newOffset; final Rect transformedDragTarget = MatrixUtils.transformRect(
const double overDragMax = 20.0; transform,
_dragTargetRelatedToScrollOrigin,
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin =
globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
final double viewportStart =
_offsetExtent(viewportOrigin, _scrollDirection);
final double viewportEnd =
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
final double proxyStart = _offsetExtent(
_dragTargetRelatedToScrollOrigin.topLeft,
_scrollDirection,
);
final double proxyEnd = _offsetExtent(
_dragTargetRelatedToScrollOrigin.bottomRight,
_scrollDirection,
);
switch (_axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
if (proxyEnd > viewportEnd &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyStart < viewportStart &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag =
math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
case AxisDirection.right:
case AxisDirection.down:
if (proxyStart < viewportStart &&
scrollable.position.pixels > scrollable.position.minScrollExtent) {
final double overDrag =
math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyEnd > viewportEnd &&
scrollable.position.pixels < scrollable.position.maxScrollExtent) {
final double overDrag = math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
}
if (newOffset == null ||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
// Drag should not trigger scroll.
_scrolling = false;
return;
}
duration ??= Duration(milliseconds: (1000 / velocityScalar).round());
if (duration == Duration.zero) {
scrollable.position.jumpTo(newOffset);
} else {
await scrollable.position.animateTo(
newOffset,
duration: duration,
curve: Curves.linear,
); );
}
if (onScrollViewScrolled != null) { if ((globalRect.size.width + precisionErrorTolerance) >=
onScrollViewScrolled!(); transformedDragTarget.size.width &&
} (globalRect.size.height + precisionErrorTolerance) >=
if (_scrolling) { transformedDragTarget.size.height) {
await _scroll(duration: duration); // do nothing.
}
_scrolling = true;
double? newOffset;
const double overDragMax = 20.0;
final Offset deltaToOrigin = scrollable.deltaToScrollOrigin;
final Offset viewportOrigin =
globalRect.topLeft.translate(deltaToOrigin.dx, deltaToOrigin.dy);
final double viewportStart =
_offsetExtent(viewportOrigin, _scrollDirection);
final double viewportEnd =
viewportStart + _sizeExtent(globalRect.size, _scrollDirection);
final double proxyStart = _offsetExtent(
_dragTargetRelatedToScrollOrigin.topLeft,
_scrollDirection,
);
final double proxyEnd = _offsetExtent(
_dragTargetRelatedToScrollOrigin.bottomRight,
_scrollDirection,
);
switch (_axisDirection) {
case AxisDirection.up:
case AxisDirection.left:
if (proxyEnd > viewportEnd &&
scrollable.position.pixels >
scrollable.position.minScrollExtent) {
final double overDrag =
math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyStart < viewportStart &&
scrollable.position.pixels <
scrollable.position.maxScrollExtent) {
final double overDrag =
math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
case AxisDirection.right:
case AxisDirection.down:
if (proxyStart < viewportStart &&
scrollable.position.pixels >
scrollable.position.minScrollExtent) {
final double overDrag =
math.min(viewportStart - proxyStart, overDragMax);
newOffset = math.max(
scrollable.position.minScrollExtent,
scrollable.position.pixels - overDrag,
);
} else if (proxyEnd > viewportEnd &&
scrollable.position.pixels <
scrollable.position.maxScrollExtent) {
final double overDrag =
math.min(proxyEnd - viewportEnd, overDragMax);
newOffset = math.min(
scrollable.position.maxScrollExtent,
scrollable.position.pixels + overDrag,
);
}
}
if (newOffset == null ||
(newOffset - scrollable.position.pixels).abs() < 1.0) {
// Drag should not trigger scroll.
_scrolling = false;
return;
}
final Duration duration =
Duration(milliseconds: (1000 / velocityScalar).round());
await scrollable.position
.animateTo(newOffset, duration: duration, curve: Curves.linear);
onScrollViewScrolled?.call();
if (_scrolling) {
await _scroll();
}
} catch (e) {
debugPrint(e.toString());
} finally {
_scrolling = false;
} }
} }
} }
@ -329,10 +336,83 @@ typedef ScrollIncrementCalculator = double Function(
ScrollIncrementDetails details, ScrollIncrementDetails details,
); );
/// An [Action] that scrolls the [Scrollable] that encloses the current /// Describes the type of scroll increment that will be performed by a
/// [primaryFocus] by the amount configured in the [ScrollIntent] given to it. /// [ScrollAction] on a [Scrollable].
/// ///
/// If a Scrollable cannot be found above the current [primaryFocus], the /// This is used to configure a [ScrollIncrementDetails] object to pass to a
/// [ScrollIncrementCalculator] function on a [Scrollable].
///
/// {@template flutter.widgets.ScrollIncrementType.intent}
/// This indicates the *intent* of the scroll, not necessarily the size. Not all
/// scrollable areas will have the concept of a "line" or "page", but they can
/// respond to the different standard key bindings that cause scrolling, which
/// are bound to keys that people use to indicate a "line" scroll (e.g.
/// control-arrowDown keys) or a "page" scroll (e.g. pageDown key). It is
/// recommended that at least the relative magnitudes of the scrolls match
/// expectations.
/// {@endtemplate}
// enum ScrollIncrementType {
// /// Indicates that the [ScrollIncrementCalculator] should return the scroll
// /// distance it should move when the user requests to scroll by a "line".
// ///
// /// The distance a "line" scrolls refers to what should happen when the key
// /// binding for "scroll down/up by a line" is triggered. It's up to the
// /// [ScrollIncrementCalculator] function to decide what that means for a
// /// particular scrollable.
// line,
// /// Indicates that the [ScrollIncrementCalculator] should return the scroll
// /// distance it should move when the user requests to scroll by a "page".
// ///
// /// The distance a "page" scrolls refers to what should happen when the key
// /// binding for "scroll down/up by a page" is triggered. It's up to the
// /// [ScrollIncrementCalculator] function to decide what that means for a
// /// particular scrollable.
// page,
// }
/// A details object that describes the type of scroll increment being requested
/// of a [ScrollIncrementCalculator] function, as well as the current metrics
// /// for the scrollable.
// class ScrollIncrementDetails {
// /// A const constructor for a [ScrollIncrementDetails].
// const ScrollIncrementDetails({required this.type, required this.metrics});
// /// The type of scroll this is (e.g. line, page, etc.).
// ///
// /// {@macro flutter.widgets.ScrollIncrementType.intent}
// final ScrollIncrementType type;
// /// The current metrics of the scrollable that is being scrolled.
// final ScrollMetrics metrics;
// }
/// An [Intent] that represents scrolling the nearest scrollable by an amount
/// appropriate for the [type] specified.
///
/// The actual amount of the scroll is determined by the
/// [Scrollable.incrementCalculator], or by its defaults if that is not
/// specified.
class ScrollIntent extends Intent {
/// Creates a const [ScrollIntent] that requests scrolling in the given
/// [direction], with the given [type].
const ScrollIntent({
required this.direction,
this.type = ScrollIncrementType.line,
});
/// The direction in which to scroll the scrollable containing the focused
/// widget.
final AxisDirection direction;
/// The type of scrolling that is intended.
final ScrollIncrementType type;
}
/// An [Action] that scrolls the relevant [Scrollable] by the amount configured
/// in the [ScrollIntent] given to it.
///
/// If a Scrollable cannot be found above the given [BuildContext], the
/// [PrimaryScrollController] will be considered for default handling of /// [PrimaryScrollController] will be considered for default handling of
/// [ScrollAction]s. /// [ScrollAction]s.
/// ///
@ -340,23 +420,19 @@ typedef ScrollIncrementCalculator = double Function(
/// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the /// for a [ScrollIntent.type] set to [ScrollIncrementType.page] is 80% of the
/// size of the scroll window, and for [ScrollIncrementType.line], 50 logical /// size of the scroll window, and for [ScrollIncrementType.line], 50 logical
/// pixels. /// pixels.
class ScrollAction extends Action<ScrollIntent> { class ScrollAction extends ContextAction<ScrollIntent> {
@override @override
bool isEnabled(ScrollIntent intent) { bool isEnabled(ScrollIntent intent, [BuildContext? context]) {
final FocusNode? focus = primaryFocus; if (context == null) {
final bool contextIsValid = focus != null && focus.context != null; return false;
if (contextIsValid) {
// Check for primary scrollable within the current context
if (Scrollable.maybeOf(focus.context!) != null) {
return true;
}
// Check for fallback scrollable with context from PrimaryScrollController
final ScrollController? primaryScrollController =
PrimaryScrollController.maybeOf(focus.context!);
return primaryScrollController != null &&
primaryScrollController.hasClients;
} }
return false; if (Scrollable.maybeOf(context) != null) {
return true;
}
final ScrollController? primaryScrollController =
PrimaryScrollController.maybeOf(context);
return (primaryScrollController != null) &&
primaryScrollController.hasClients;
} }
/// Returns the scroll increment for a single scroll request, for use when /// Returns the scroll increment for a single scroll request, for use when
@ -364,8 +440,8 @@ class ScrollAction extends Action<ScrollIntent> {
/// ///
/// Must not be called when the position is null, or when any of the position /// Must not be called when the position is null, or when any of the position
/// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are /// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
/// null. The type and state arguments must not be null, and the widget must /// null. The widget must have already been laid out so that the position
/// have already been laid out so that the position fields are valid. /// fields are valid.
static double _calculateScrollIncrement( static double _calculateScrollIncrement(
ScrollableState state, { ScrollableState state, {
ScrollIncrementType type = ScrollIncrementType.line, ScrollIncrementType type = ScrollIncrementType.line,
@ -377,18 +453,13 @@ class ScrollAction extends Action<ScrollIntent> {
); );
if (state.widget.incrementCalculator != null) { if (state.widget.incrementCalculator != null) {
return state.widget.incrementCalculator!( return state.widget.incrementCalculator!(
ScrollIncrementDetails( ScrollIncrementDetails(type: type, metrics: state.position),
type: type,
metrics: state.position,
),
); );
} }
switch (type) { return switch (type) {
case ScrollIncrementType.line: ScrollIncrementType.line => 50.0,
return 50.0; ScrollIncrementType.page => 0.8 * state.position.viewportDimension,
case ScrollIncrementType.page: };
return 0.8 * state.position.viewportDimension;
}
} }
/// Find out how much of an increment to move by, taking the different /// Find out how much of an increment to move by, taking the different
@ -397,58 +468,22 @@ class ScrollAction extends Action<ScrollIntent> {
ScrollableState state, ScrollableState state,
ScrollIntent intent, ScrollIntent intent,
) { ) {
final double increment = if (axisDirectionToAxis(intent.direction) ==
_calculateScrollIncrement(state, type: intent.type); axisDirectionToAxis(state.axisDirection)) {
switch (intent.direction) { final double increment =
case AxisDirection.down: _calculateScrollIncrement(state, type: intent.type);
switch (state.axisDirection) { return intent.direction == state.axisDirection ? increment : -increment;
case AxisDirection.up:
return -increment;
case AxisDirection.down:
return increment;
case AxisDirection.right:
case AxisDirection.left:
return 0.0;
}
case AxisDirection.up:
switch (state.axisDirection) {
case AxisDirection.up:
return increment;
case AxisDirection.down:
return -increment;
case AxisDirection.right:
case AxisDirection.left:
return 0.0;
}
case AxisDirection.left:
switch (state.axisDirection) {
case AxisDirection.right:
return -increment;
case AxisDirection.left:
return increment;
case AxisDirection.up:
case AxisDirection.down:
return 0.0;
}
case AxisDirection.right:
switch (state.axisDirection) {
case AxisDirection.right:
return increment;
case AxisDirection.left:
return -increment;
case AxisDirection.up:
case AxisDirection.down:
return 0.0;
}
} }
return 0.0;
} }
@override @override
void invoke(ScrollIntent intent) { void invoke(ScrollIntent intent, [BuildContext? context]) {
ScrollableState? state = Scrollable.maybeOf(primaryFocus!.context!); assert(context != null, 'Cannot scroll without a context.');
ScrollableState? state = Scrollable.maybeOf(context!);
if (state == null) { if (state == null) {
final ScrollController primaryScrollController = final ScrollController primaryScrollController =
PrimaryScrollController.of(primaryFocus!.context!); PrimaryScrollController.of(context);
assert(() { assert(() {
if (primaryScrollController.positions.length != 1) { if (primaryScrollController.positions.length != 1) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
@ -472,29 +507,22 @@ class ScrollAction extends Action<ScrollIntent> {
return true; return true;
}()); }());
if (primaryScrollController.position.context.notificationContext == final BuildContext? notificationContext =
null && primaryScrollController.position.context.notificationContext;
Scrollable.maybeOf( if (notificationContext != null) {
primaryScrollController.position.context.notificationContext!, state = Scrollable.maybeOf(notificationContext);
) == }
null) { if (state == null) {
return; return;
} }
state = Scrollable.maybeOf(
primaryScrollController.position.context.notificationContext!,
);
} }
assert( assert(
state != null, state.position.hasPixels,
'$ScrollAction was invoked on a context that has no scrollable parent',
);
assert(
state!.position.hasPixels,
'Scrollable must be laid out before it can be scrolled via a ScrollAction', 'Scrollable must be laid out before it can be scrolled via a ScrollAction',
); );
// Don't do anything if the user isn't allowed to scroll. // Don't do anything if the user isn't allowed to scroll.
if (state!.resolvedPhysics != null && if (state.resolvedPhysics != null &&
!state.resolvedPhysics!.shouldAcceptUserOffset(state.position)) { !state.resolvedPhysics!.shouldAcceptUserOffset(state.position)) {
return; return;
} }

View file

@ -366,6 +366,19 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
element.children.length == 1 && element.children.length == 1 &&
element.children.first.localName == HTMLTags.paragraph) { element.children.first.localName == HTMLTags.paragraph) {
(delta, node) = _parseDeltaElement(element.children.first, type: type); (delta, node) = _parseDeltaElement(element.children.first, type: type);
} else if (delta.isEmpty &&
element.children.isNotEmpty &&
element.children.first.localName == HTMLTags.paragraph) {
final paragraphElement = element.children.first;
(delta, _) = _parseDeltaElement(paragraphElement, type: type);
final remainingChildren = element.children.skip(1);
node = remainingChildren.expand((child) {
if (HTMLTags.specialElements.contains(child.localName)) {
return _parseSpecialElements(child, type: type);
}
return <Node>[];
}).toList();
} }
return Node( return Node(
type: type, type: type,
@ -401,7 +414,8 @@ class DocumentHTMLDecoder extends Converter<String, Document> {
for (final child in children) { for (final child in children) {
if (child is dom.Element) { if (child is dom.Element) {
if (child.children.isNotEmpty && if (child.children.isNotEmpty &&
HTMLTags.formattingElements.contains(child.localName) == false) { HTMLTags.formattingElements.contains(child.localName) == false &&
HTMLTags.specialElements.contains(child.localName) == false) {
//rich editor for webs do this so handling that case for href <a href="https://www.google.com" rel="noopener noreferrer" target="_blank"><strong><em><u>demo</u></em></strong></a> //rich editor for webs do this so handling that case for href <a href="https://www.google.com" rel="noopener noreferrer" target="_blank"><strong><em><u>demo</u></em></strong></a>
nodes.addAll(_parseElement(child.children, type: type)); nodes.addAll(_parseElement(child.children, type: type));

View file

@ -0,0 +1,45 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('customer html document test', () {
test('paste text from ChatGPT', () {
const html = '''
<meta charset='utf-8'><h2 data-start="283" data-end="314">Key Pain Points &amp; Challenges</h2>
<ol data-start="316" data-end="4630">
<li data-start="316" data-end="870">
<p data-start="319" data-end="361"><strong data-start="319" data-end="359">Accuracy, Hallucinations &amp; Omissions</strong></p>
<ul data-start="365" data-end="870">
<li data-start="365" data-end="569">
<p data-start="367" data-end="569">AI may mis-transcribe or “hallucinate” content (i.e. generate text that was never spoken). In high-stakes settings (e.g. medical, legal) this is especially risky. <span class="" data-state="closed"><span class="ms-1 inline-flex max-w-full items-center relative top-[-0.094rem] animate-[show_150ms_ease-in]" data-testid="webpage-citation-pill"><a href="https://apnews.com/article/90020cdf5fa16c79ca2e5b6c4c9bbb14?utm_source=chatgpt.com" target="_blank" rel="noopener" alt="https://apnews.com/article/90020cdf5fa16c79ca2e5b6c4c9bbb14?utm_source=chatgpt.com" class="flex h-4.5 overflow-hidden rounded-xl px-2 text-[9px] font-medium transition-colors duration-150 ease-in-out text-token-text-secondary! bg-[#F4F4F4]! dark:bg-[#303030]!"><span class="relative start-0 bottom-0 flex h-full w-full items-center"><span class="flex h-4 w-full items-center justify-between absolute"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">Elite Asia</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+5</span></span><span class="flex h-4 w-full items-center justify-between"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">AP News</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+5</span></span><span class="flex h-4 w-full items-center justify-between absolute"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">WIRED</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+5</span></span></span></a></span></span></p>
</li>
<li data-start="573" data-end="743">
<p data-start="575" data-end="743">Technical jargon, acronyms, domain-specific vocabulary, accents, dialects, or overlapping speech still lead to high error rates. <span class="" data-state="closed"><span class="ms-1 inline-flex max-w-full items-center relative top-[-0.094rem] animate-[show_150ms_ease-in]" data-testid="webpage-citation-pill"><a href="https://go.verbit.ai/blog/automatic-transcription-the-pros-and-cons-of-ai-solutions/?utm_source=chatgpt.com" target="_blank" rel="noopener" alt="https://go.verbit.ai/blog/automatic-transcription-the-pros-and-cons-of-ai-solutions/?utm_source=chatgpt.com" class="flex h-4.5 overflow-hidden rounded-xl px-2 text-[9px] font-medium transition-colors duration-150 ease-in-out text-token-text-secondary! bg-[#F4F4F4]! dark:bg-[#303030]!"><span class="relative start-0 bottom-0 flex h-full w-full items-center"><span class="flex h-4 w-full items-center justify-between absolute"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">blog.huddles.app</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+3</span></span><span class="flex h-4 w-full items-center justify-between"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">go.verbit.ai</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+3</span></span><span class="flex h-4 w-full items-center justify-between absolute"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">eff-inc.com</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+3</span></span></span></a></span></span></p>
</li>
<li data-start="747" data-end="870">
<p data-start="749" data-end="870">Silence, nonverbal cues, or pauses are sometimes incorrectly annotated or filled. <span class="" data-state="closed"><span class="ms-1 inline-flex max-w-full items-center relative top-[-0.094rem] animate-[show_150ms_ease-in]" data-testid="webpage-citation-pill"><a href="https://blog.huddles.app/what-are-the-limitations-of-ai-in-taking-meeting-notes/?utm_source=chatgpt.com" target="_blank" rel="noopener" alt="https://blog.huddles.app/what-are-the-limitations-of-ai-in-taking-meeting-notes/?utm_source=chatgpt.com" class="flex h-4.5 overflow-hidden rounded-xl px-2 text-[9px] font-medium transition-colors duration-150 ease-in-out text-token-text-secondary! bg-[#F4F4F4]! dark:bg-[#303030]!"><span class="relative start-0 bottom-0 flex h-full w-full items-center"><span class="flex h-4 w-full items-center justify-between"><span class="max-w-[15ch] grow truncate overflow-hidden text-center">blog.huddles.app</span><span class="-me-1 flex h-full items-center rounded-full px-1 text-[#8F8F8F]">+1</span></span></span></a></span></span></p></li></ul></li></ol>
''';
final document = htmlToDocument(html);
final children = document.root.children;
expect(children.length, 2);
// heading 2 - Key Pain Points & Challenges
expect(children[0].type, HeadingBlockKeys.type);
expect(children[0].delta!.toPlainText(), 'Key Pain Points & Challenges');
// numbered list - Accuracy, Hallucinations & Omissions
expect(children[1].type, NumberedListBlockKeys.type);
expect(
children[1].delta!.toPlainText(),
'Accuracy, Hallucinations & Omissions',
);
// its children - 3 bulleted list items
final children1 = children[1].children;
expect(children1.length, 3);
expect(
children1.every((child) => child.type == BulletedListBlockKeys.type),
isTrue,
);
});
});
}