Skip to content

Commit ab35355

Browse files
committed
feat: implement find the forward nearest text node
1 parent c5af7db commit ab35355

File tree

3 files changed

+189
-2
lines changed

3 files changed

+189
-2
lines changed

frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ class RichTextNodeWidget extends BuiltInTextWidget {
4343
// customize
4444

4545
class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
46-
with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
46+
with
47+
SelectableMixin,
48+
DefaultSelectable,
49+
BuiltInStyleMixin,
50+
BuiltInTextWidgetMixin {
4751
@override
4852
GlobalKey? get iconKey => null;
4953

@@ -59,7 +63,7 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
5963
}
6064

6165
@override
62-
Widget build(BuildContext context) {
66+
Widget buildWithSingle(BuildContext context) {
6367
return Padding(
6468
padding: padding,
6569
child: FlowyRichText(

frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,3 +288,55 @@ Node? _closestTextNode(Node? node) {
288288
}
289289
return null;
290290
}
291+
292+
TextNode? findLastTextNode(Node node) {
293+
final children = node.children.toList(growable: false).reversed;
294+
for (final child in children) {
295+
if (child.children.isNotEmpty) {
296+
final result = findLastTextNode(child);
297+
if (result != null) {
298+
return result;
299+
}
300+
}
301+
if (child is TextNode) {
302+
return child;
303+
}
304+
}
305+
if (node is TextNode) {
306+
return node;
307+
}
308+
return null;
309+
}
310+
311+
// find the forward nearest text node
312+
TextNode? forwardNearestTextNode(Node node) {
313+
var previous = node.previous;
314+
while (previous != null) {
315+
final lastTextNode = findLastTextNode(previous);
316+
if (lastTextNode != null) {
317+
return lastTextNode;
318+
}
319+
if (previous is TextNode) {
320+
return previous;
321+
}
322+
previous = previous.previous;
323+
}
324+
final parent = node.parent;
325+
if (parent != null) {
326+
if (parent is TextNode) {
327+
return parent;
328+
}
329+
return forwardNearestTextNode(parent);
330+
}
331+
return null;
332+
}
333+
334+
Node? _forwardNearestTextNode(Node node) {
335+
if (node is TextNode) {
336+
return node;
337+
}
338+
if (node.next != null) {
339+
return _forwardNearestTextNode(node.next!);
340+
}
341+
return null;
342+
}

frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import 'dart:collection';
2+
13
import 'package:appflowy_editor/appflowy_editor.dart';
24
import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
5+
import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
6+
import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
37
import 'package:flutter/services.dart';
48
import 'package:flutter_test/flutter_test.dart';
59
import 'package:network_image_mock/network_image_mock.dart';
@@ -320,6 +324,133 @@ void main() async {
320324
);
321325
expect((editor.nodeAtPath([0, 0]) as TextNode).toRawString(), text * 2);
322326
});
327+
328+
testWidgets('Delete the complicated nested bulleted list', (tester) async {
329+
// * Welcome to Appflowy 😁
330+
// * Welcome to Appflowy 😁
331+
// * Welcome to Appflowy 😁
332+
// * Welcome to Appflowy 😁
333+
// * Welcome to Appflowy 😁
334+
const text = 'Welcome to Appflowy 😁';
335+
final node = TextNode(
336+
type: 'text',
337+
delta: Delta()..insert(text),
338+
attributes: {
339+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
340+
},
341+
);
342+
343+
node
344+
..insert(
345+
node.copyWith(children: LinkedList()),
346+
)
347+
..insert(
348+
node.copyWith(children: LinkedList())
349+
..insert(
350+
node.copyWith(children: LinkedList()),
351+
)
352+
..insert(
353+
node.copyWith(children: LinkedList()),
354+
),
355+
);
356+
357+
final editor = tester.editor..insert(node);
358+
await editor.startTesting();
359+
360+
await editor.updateSelection(
361+
Selection.single(path: [0, 1], startOffset: 0),
362+
);
363+
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
364+
expect(
365+
editor.nodeAtPath([0, 1])!.subtype != BuiltInAttributeKey.bulletedList,
366+
true,
367+
);
368+
expect(
369+
editor.nodeAtPath([0, 1, 0])!.subtype,
370+
BuiltInAttributeKey.bulletedList,
371+
);
372+
expect(
373+
editor.nodeAtPath([0, 1, 1])!.subtype,
374+
BuiltInAttributeKey.bulletedList,
375+
);
376+
expect(find.byType(FlowyRichText), findsNWidgets(5));
377+
378+
// Before
379+
// * Welcome to Appflowy 😁
380+
// * Welcome to Appflowy 😁
381+
// Welcome to Appflowy 😁
382+
// * Welcome to Appflowy 😁
383+
// * Welcome to Appflowy 😁
384+
// After
385+
// * Welcome to Appflowy 😁
386+
// * Welcome to Appflowy 😁
387+
// Welcome to Appflowy 😁
388+
// * Welcome to Appflowy 😁
389+
// * Welcome to Appflowy 😁
390+
await editor.pressLogicKey(LogicalKeyboardKey.backspace);
391+
expect(
392+
editor.nodeAtPath([1])!.subtype != BuiltInAttributeKey.bulletedList,
393+
true,
394+
);
395+
expect(
396+
editor.nodeAtPath([1, 0])!.subtype == BuiltInAttributeKey.bulletedList,
397+
true,
398+
);
399+
expect(
400+
editor.nodeAtPath([1, 1])!.subtype == BuiltInAttributeKey.bulletedList,
401+
true,
402+
);
403+
404+
// After
405+
// * Welcome to Appflowy 😁
406+
// * Welcome to Appflowy 😁Welcome to Appflowy 😁
407+
// * Welcome to Appflowy 😁
408+
// * Welcome to Appflowy 😁
409+
});
410+
411+
test('find the last text node', () {
412+
// * Welcome to Appflowy 😁
413+
// * Welcome to Appflowy 😁
414+
// * Welcome to Appflowy 😁
415+
// * Welcome to Appflowy 😁
416+
// * Welcome to Appflowy 😁
417+
// * Welcome to Appflowy 😁
418+
// * Welcome to Appflowy 😁
419+
const text = 'Welcome to Appflowy 😁';
420+
TextNode textNode() {
421+
return TextNode(
422+
type: 'text',
423+
delta: Delta()..insert(text),
424+
);
425+
}
426+
427+
final node110 = textNode();
428+
final node111 = textNode();
429+
final node11 = textNode()
430+
..insert(node110)
431+
..insert(node111);
432+
final node10 = textNode();
433+
final node1 = textNode()
434+
..insert(node10)
435+
..insert(node11);
436+
final node0 = textNode();
437+
final node = textNode()
438+
..insert(node0)
439+
..insert(node1);
440+
441+
expect(findLastTextNode(node)?.path, [1, 1, 1]);
442+
expect(findLastTextNode(node0)?.path, [0]);
443+
expect(findLastTextNode(node1)?.path, [1, 1, 1]);
444+
expect(findLastTextNode(node10)?.path, [1, 0]);
445+
expect(findLastTextNode(node11)?.path, [1, 1, 1]);
446+
447+
expect(forwardNearestTextNode(node111)?.path, [1, 1, 0]);
448+
expect(forwardNearestTextNode(node110)?.path, [1, 1]);
449+
expect(forwardNearestTextNode(node11)?.path, [1, 0]);
450+
expect(forwardNearestTextNode(node10)?.path, [1]);
451+
expect(forwardNearestTextNode(node1)?.path, [0]);
452+
expect(forwardNearestTextNode(node0)?.path, []);
453+
});
323454
}
324455

325456
Future<void> _deleteFirstImage(WidgetTester tester, bool isBackward) async {

0 commit comments

Comments
 (0)