feat: implement node extension, allSatisfyInSelection
This commit is contained in:
parent
683318b177
commit
2bf07055ed
19 changed files with 563 additions and 203 deletions
3
assets/images/toolbar/numbered_list.svg
Normal file
3
assets/images/toolbar/numbered_list.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -58,3 +58,4 @@ export 'src/editor/editor_component/editor_component.dart';
|
||||||
export 'src/editor/command/transform.dart';
|
export 'src/editor/command/transform.dart';
|
||||||
export 'src/editor/util/util.dart';
|
export 'src/editor/util/util.dart';
|
||||||
export 'src/editor/toolbar/toolbar.dart';
|
export 'src/editor/toolbar/toolbar.dart';
|
||||||
|
export 'src/extensions/node_extensions.dart';
|
||||||
|
|
|
||||||
18
lib/src/editor/toolbar/items/bulleted_list_toolbar_item.dart
Normal file
18
lib/src/editor/toolbar/items/bulleted_list_toolbar_item.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/items/icon_item_widget.dart';
|
||||||
|
|
||||||
|
ToolbarItem paragraphItem = ToolbarItem(
|
||||||
|
id: 'editor.paragraph',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'bulleted_list';
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/bulleted_list',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.bulletedList,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
38
lib/src/editor/toolbar/items/format_toolbar_items.dart
Normal file
38
lib/src/editor/toolbar/items/format_toolbar_items.dart
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/items/icon_item_widget.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
ToolbarItem underlineItem = ToolbarItem(
|
||||||
|
id: 'editor.paragraph',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'quote';
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/bold',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip:
|
||||||
|
'${AppFlowyEditorLocalizations.current.bold}${_shortcutTooltips('⌘ + B', 'CTRL + B', 'CTRL + B')}',
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
String _shortcutTooltips(
|
||||||
|
String? macOSString,
|
||||||
|
String? windowsString,
|
||||||
|
String? linuxString,
|
||||||
|
) {
|
||||||
|
if (kIsWeb) return '';
|
||||||
|
if (Platform.isMacOS && macOSString != null) {
|
||||||
|
return '\n$macOSString';
|
||||||
|
} else if (Platform.isWindows && windowsString != null) {
|
||||||
|
return '\n$windowsString';
|
||||||
|
} else if (Platform.isLinux && linuxString != null) {
|
||||||
|
return '\n$linuxString';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
50
lib/src/editor/toolbar/items/heading_toolbar_items.dart
Normal file
50
lib/src/editor/toolbar/items/heading_toolbar_items.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/items/icon_item_widget.dart';
|
||||||
|
|
||||||
|
ToolbarItem heading1Item = ToolbarItem(
|
||||||
|
id: 'editor.h1',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'heading' && node.attributes['level'] == 1;
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/h1',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.heading1,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ToolbarItem heading2Item = ToolbarItem(
|
||||||
|
id: 'editor.h2',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'heading' && node.attributes['level'] == 2;
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/h2',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.heading2,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ToolbarItem heading3Item = ToolbarItem(
|
||||||
|
id: 'editor.h3',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'heading' && node.attributes['level'] == 3;
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/h3',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.heading3,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
||||||
class IconItemWidget extends StatelessWidget {
|
class IconItemWidget extends StatelessWidget {
|
||||||
const IconItemWidget({
|
const IconItemWidget({
|
||||||
super.key,
|
super.key,
|
||||||
this.size = const Size.square(28.0),
|
this.size = const Size.square(32.0),
|
||||||
required this.iconName,
|
required this.iconName,
|
||||||
required this.isHighlight,
|
required this.isHighlight,
|
||||||
this.tooltip,
|
this.tooltip,
|
||||||
|
|
|
||||||
18
lib/src/editor/toolbar/items/numbered_list_toolbar_item.dart
Normal file
18
lib/src/editor/toolbar/items/numbered_list_toolbar_item.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/items/icon_item_widget.dart';
|
||||||
|
|
||||||
|
ToolbarItem paragraphItem = ToolbarItem(
|
||||||
|
id: 'editor.paragraph',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'numbered_list';
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/numbered_list',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.numberedList,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
18
lib/src/editor/toolbar/items/quote_toolbar_item.dart
Normal file
18
lib/src/editor/toolbar/items/quote_toolbar_item.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_editor/src/editor/toolbar/items/icon_item_widget.dart';
|
||||||
|
|
||||||
|
ToolbarItem paragraphItem = ToolbarItem(
|
||||||
|
id: 'editor.paragraph',
|
||||||
|
isActive: (editorState) => editorState.selection?.isSingle ?? false,
|
||||||
|
builder: (context, editorState) {
|
||||||
|
final selection = editorState.selection!;
|
||||||
|
final node = editorState.getNodeAtPath(selection.start.path)!;
|
||||||
|
final isHighlight = node.type == 'quote';
|
||||||
|
return IconItemWidget(
|
||||||
|
iconName: 'toolbar/quote',
|
||||||
|
isHighlight: isHighlight,
|
||||||
|
tooltip: AppFlowyEditorLocalizations.current.quote,
|
||||||
|
onPressed: () {},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export 'desktop/floating_toolbar.dart';
|
export 'desktop/floating_toolbar.dart';
|
||||||
export 'desktop/floating_toolbar_widget.dart';
|
export 'desktop/floating_toolbar_widget.dart';
|
||||||
|
|
||||||
export 'items/heading_toolbar_item.dart';
|
export 'items/heading_toolbar_items.dart';
|
||||||
export 'items/paragraph_toolbar_item.dart';
|
export 'items/paragraph_toolbar_item.dart';
|
||||||
export 'items/placeholder_toolbar_item.dart';
|
export 'items/placeholder_toolbar_item.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
|
||||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
|
||||||
import 'package:appflowy_editor/src/editor_state.dart';
|
|
||||||
import 'package:appflowy_editor/src/extensions/object_extensions.dart';
|
|
||||||
import 'package:appflowy_editor/src/render/selection/selectable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
extension NodeExtensions on Node {
|
extension NodeExtensions on Node {
|
||||||
|
|
@ -76,6 +71,26 @@ extension NodeExtensions on Node {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allSatisfyInSelection(
|
||||||
|
Selection selection,
|
||||||
|
bool Function(Delta delta) test,
|
||||||
|
) {
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = selection.normalized;
|
||||||
|
|
||||||
|
var delta = this.delta;
|
||||||
|
if (delta == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delta = delta.slice(selection.startIndex, selection.endIndex);
|
||||||
|
|
||||||
|
return test(delta);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NodesExtensions<T extends Node> on List<T> {
|
extension NodesExtensions<T extends Node> on List<T> {
|
||||||
|
|
@ -90,4 +105,39 @@ extension NodesExtensions<T extends Node> on List<T> {
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool allSatisfyInSelection(
|
||||||
|
Selection selection,
|
||||||
|
bool Function(Delta delta) test,
|
||||||
|
) {
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
selection = selection.normalized;
|
||||||
|
final nodes = this.normalized;
|
||||||
|
|
||||||
|
if (nodes.length == 1) {
|
||||||
|
return nodes.first.allSatisfyInSelection(selection, test);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
final node = nodes[i];
|
||||||
|
var delta = node.delta;
|
||||||
|
if (delta == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i == 0) {
|
||||||
|
delta = delta.slice(selection.start.offset);
|
||||||
|
} else if (i == nodes.length - 1) {
|
||||||
|
delta = delta.slice(0, selection.end.offset);
|
||||||
|
}
|
||||||
|
if (!test(delta)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ void main() async {
|
||||||
initialText: text,
|
initialText: text,
|
||||||
)
|
)
|
||||||
.addParagraph(
|
.addParagraph(
|
||||||
builder: (index) => '1.$text',
|
builder: (index) => Delta()..insert('1.$text'),
|
||||||
);
|
);
|
||||||
final editorState = EditorState(document: document);
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ Future<void> testFormatCharacterShortcut(
|
||||||
String text = 'Welcome to AppFlowy Editor 🔥!',
|
String text = 'Welcome to AppFlowy Editor 🔥!',
|
||||||
}) async {
|
}) async {
|
||||||
final document = Document.blank().addParagraph(
|
final document = Document.blank().addParagraph(
|
||||||
builder: (index) => '$prefix$text',
|
builder: (index) => Delta()..insert('$prefix$text'),
|
||||||
);
|
);
|
||||||
final editorState = EditorState(document: document);
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,147 +17,151 @@ void main() async {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
group('selection_transform.dart', () {
|
group('deleteSelection', () {
|
||||||
group('deleteSelection', () {
|
test('the selection is collapsed', () async {
|
||||||
test('the selection is collapsed', () async {
|
final document = Document.blank().addParagraphs(
|
||||||
final document = Document.blank().addParagraphs(
|
3,
|
||||||
3,
|
builder: (index) =>
|
||||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||||
);
|
);
|
||||||
final editorState = EditorState(document: document);
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
final selection = Selection.collapsed(
|
final selection = Selection.collapsed(
|
||||||
Position(path: [1], offset: 10),
|
Position(path: [1], offset: 10),
|
||||||
);
|
);
|
||||||
final before = editorState.getNodesInSelection(selection).first;
|
final before = editorState.getNodesInSelection(selection).first;
|
||||||
final result = await editorState.deleteSelection(selection);
|
final result = await editorState.deleteSelection(selection);
|
||||||
|
|
||||||
// nothing happens
|
// nothing happens
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
final after = editorState.getNodesInSelection(selection).first;
|
final after = editorState.getNodesInSelection(selection).first;
|
||||||
expect(
|
expect(
|
||||||
before.toJson(),
|
before.toJson(),
|
||||||
after.toJson(),
|
after.toJson(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the selection is single', () async {
|
test('the selection is single', () async {
|
||||||
final document = Document.blank().addParagraphs(
|
final document = Document.blank().addParagraphs(
|
||||||
3,
|
3,
|
||||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
builder: (index) =>
|
||||||
);
|
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||||
final editorState = EditorState(document: document);
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
// |Welcome|
|
// |Welcome|
|
||||||
final selection = Selection.single(
|
final selection = Selection.single(
|
||||||
path: [1],
|
path: [1],
|
||||||
startOffset: 3,
|
startOffset: 3,
|
||||||
endOffset: 10,
|
endOffset: 10,
|
||||||
);
|
);
|
||||||
final result = await editorState.deleteSelection(selection);
|
final result = await editorState.deleteSelection(selection);
|
||||||
|
|
||||||
// nothing happens
|
// nothing happens
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
expect(editorState.selection, selection.collapse(atStart: true));
|
expect(editorState.selection, selection.collapse(atStart: true));
|
||||||
final after = editorState.getNodesInSelection(selection).first;
|
final after = editorState.getNodesInSelection(selection).first;
|
||||||
expect(after.delta?.toPlainText(), '1. to AppFlowy Editor 🔥!');
|
expect(after.delta?.toPlainText(), '1. to AppFlowy Editor 🔥!');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the selection is not single and not collapsed - 1', () async {
|
test('the selection is not single and not collapsed - 1', () async {
|
||||||
final document = Document.blank().addParagraphs(
|
final document = Document.blank().addParagraphs(
|
||||||
3,
|
3,
|
||||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
builder: (index) =>
|
||||||
);
|
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||||
final editorState = EditorState(document: document);
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
// |Welcome
|
// |Welcome
|
||||||
// ...
|
// ...
|
||||||
// Welcome|
|
// Welcome|
|
||||||
final selection = Selection(
|
final selection = Selection(
|
||||||
start: Position(
|
start: Position(
|
||||||
path: [0],
|
path: [0],
|
||||||
offset: 3,
|
offset: 3,
|
||||||
),
|
),
|
||||||
end: Position(
|
end: Position(
|
||||||
path: [2],
|
path: [2],
|
||||||
offset: 10,
|
offset: 10,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
final result = await editorState.deleteSelection(selection);
|
final result = await editorState.deleteSelection(selection);
|
||||||
|
|
||||||
// nothing happens
|
// nothing happens
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
expect(editorState.selection, selection.collapse(atStart: true));
|
expect(editorState.selection, selection.collapse(atStart: true));
|
||||||
final length = editorState.document.root.children.length;
|
final length = editorState.document.root.children.length;
|
||||||
expect(length, 1);
|
expect(length, 1);
|
||||||
expect(
|
expect(
|
||||||
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
||||||
'0. to AppFlowy Editor 🔥!',
|
'0. to AppFlowy Editor 🔥!',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Before
|
||||||
|
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||||
|
// 0.0. Welcome |to AppFlowy Editor 🔥!
|
||||||
|
// 0.0.0. Welcome to AppFlowy Editor 🔥!
|
||||||
|
// 0.1. Welcome to AppFlowy Editor 🔥!
|
||||||
|
// After
|
||||||
|
// 0. Welcome to AppFlowy Editor 🔥!
|
||||||
|
// 0.0.0. Welcome to AppFlowy Editor 🔥!
|
||||||
|
// 0.1. Welcome to AppFlowy Editor 🔥!
|
||||||
|
test('the selection is not single and not collapsed - 2', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) =>
|
||||||
|
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||||
|
decorator: (index, node) {
|
||||||
|
node.addParagraphs(
|
||||||
|
2,
|
||||||
|
builder: (index2) => Delta()
|
||||||
|
..insert('$index.$index2. Welcome to AppFlowy Editor 🔥!'),
|
||||||
|
decorator: (index2, node2) {
|
||||||
|
if (index2 == 0) {
|
||||||
|
node2.addParagraph(
|
||||||
|
builder: (index3) => Delta()
|
||||||
|
..insert(
|
||||||
|
'$index.$index2.$index3. Welcome to AppFlowy Editor 🔥!',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
// Before
|
|
||||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||||
// 0.0. Welcome |to AppFlowy Editor 🔥!
|
// 0.0. Welcome |to AppFlowy Editor 🔥!
|
||||||
// 0.0.0. Welcome to AppFlowy Editor 🔥!
|
// 0.0.0 Welcome to AppFlowy Editor 🔥!
|
||||||
// 0.1. Welcome to AppFlowy Editor 🔥!
|
// 0.1 Welcome to AppFlowy Editor 🔥!
|
||||||
// After
|
final selection = Selection(
|
||||||
// 0. Welcome to AppFlowy Editor 🔥!
|
start: Position(
|
||||||
// 0.0.0. Welcome to AppFlowy Editor 🔥!
|
path: [0],
|
||||||
// 0.1. Welcome to AppFlowy Editor 🔥!
|
offset: '0. Welcome '.length,
|
||||||
test('the selection is not single and not collapsed - 2', () async {
|
),
|
||||||
final document = Document.blank().addParagraph(
|
end: Position(
|
||||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
path: [0, 0],
|
||||||
decorator: (index, node) {
|
offset: '0.0. Welcome '.length,
|
||||||
node.addParagraphs(
|
),
|
||||||
2,
|
);
|
||||||
builder: (index2) =>
|
final result = await editorState.deleteSelection(selection);
|
||||||
'$index.$index2. Welcome to AppFlowy Editor 🔥!',
|
|
||||||
decorator: (index2, node2) {
|
|
||||||
if (index2 == 0) {
|
|
||||||
node2.addParagraph(
|
|
||||||
builder: (index3) =>
|
|
||||||
'$index.$index2.$index3. Welcome to AppFlowy Editor 🔥!',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final editorState = EditorState(document: document);
|
|
||||||
|
|
||||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
// nothing happens
|
||||||
// 0.0. Welcome |to AppFlowy Editor 🔥!
|
expect(result, true);
|
||||||
// 0.0.0 Welcome to AppFlowy Editor 🔥!
|
expect(editorState.selection, selection.collapse(atStart: true));
|
||||||
// 0.1 Welcome to AppFlowy Editor 🔥!
|
expect(
|
||||||
final selection = Selection(
|
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
||||||
start: Position(
|
'0. Welcome to AppFlowy Editor 🔥!',
|
||||||
path: [0],
|
);
|
||||||
offset: '0. Welcome '.length,
|
expect(
|
||||||
),
|
editorState.document.nodeAtPath([0, 0])?.delta?.toPlainText(),
|
||||||
end: Position(
|
'0.0.0. Welcome to AppFlowy Editor 🔥!',
|
||||||
path: [0, 0],
|
);
|
||||||
offset: '0.0. Welcome '.length,
|
expect(
|
||||||
),
|
editorState.document.nodeAtPath([0, 1])?.delta?.toPlainText(),
|
||||||
);
|
'0.1. Welcome to AppFlowy Editor 🔥!',
|
||||||
final result = await editorState.deleteSelection(selection);
|
);
|
||||||
|
|
||||||
// nothing happens
|
|
||||||
expect(result, true);
|
|
||||||
expect(editorState.selection, selection.collapse(atStart: true));
|
|
||||||
expect(
|
|
||||||
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
|
||||||
'0. Welcome to AppFlowy Editor 🔥!',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
editorState.document.nodeAtPath([0, 0])?.delta?.toPlainText(),
|
|
||||||
'0.0.0. Welcome to AppFlowy Editor 🔥!',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
editorState.document.nodeAtPath([0, 1])?.delta?.toPlainText(),
|
|
||||||
'0.1. Welcome to AppFlowy Editor 🔥!',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,71 +17,69 @@ void main() async {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
group('text_transform.dart', () {
|
group('insertNewLine', () {
|
||||||
group('insertNewLine', () {
|
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
|
||||||
|
// Before
|
||||||
|
// Welcome |to AppFlowy Editor 🔥!
|
||||||
|
// After
|
||||||
|
// Welcome
|
||||||
|
// |AppFlowy Editor 🔥!
|
||||||
|
test('insert new line at the node which doesn\'t contains children',
|
||||||
|
() async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
initialText: text,
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
// Before
|
|
||||||
// Welcome |to AppFlowy Editor 🔥!
|
// Welcome |to AppFlowy Editor 🔥!
|
||||||
// After
|
const welcome = 'Welcome ';
|
||||||
// Welcome
|
final selection = Selection.collapsed(
|
||||||
// |AppFlowy Editor 🔥!
|
Position(path: [0], offset: welcome.length),
|
||||||
test('insert new line at the node which doesn\'t contains children',
|
);
|
||||||
() async {
|
editorState.selection = selection;
|
||||||
final document = Document.blank().addParagraph(
|
editorState.insertNewLine();
|
||||||
initialText: text,
|
|
||||||
);
|
|
||||||
final editorState = EditorState(document: document);
|
|
||||||
|
|
||||||
// Welcome |to AppFlowy Editor 🔥!
|
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
||||||
const welcome = 'Welcome ';
|
expect(
|
||||||
final selection = Selection.collapsed(
|
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
||||||
Position(path: [0], offset: welcome.length),
|
text.substring(welcome.length),
|
||||||
);
|
);
|
||||||
editorState.selection = selection;
|
});
|
||||||
editorState.insertNewLine();
|
|
||||||
|
|
||||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
// Before
|
||||||
expect(
|
// Welcome |to AppFlowy Editor 🔥!
|
||||||
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
// Welcome to AppFlowy Editor 🔥!
|
||||||
text.substring(welcome.length),
|
// After
|
||||||
);
|
// Welcome |
|
||||||
});
|
// AppFlowy Editor 🔥!
|
||||||
|
// Welcome to AppFlowy Editor 🔥!
|
||||||
|
test('insert new line at the node which contains children', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
initialText: text,
|
||||||
|
decorator: (index, node) {
|
||||||
|
node.addParagraph(
|
||||||
|
initialText: text,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
// Before
|
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||||
// Welcome |to AppFlowy Editor 🔥!
|
const welcome = 'Welcome ';
|
||||||
// Welcome to AppFlowy Editor 🔥!
|
final selection = Selection.collapsed(
|
||||||
// After
|
Position(path: [0], offset: welcome.length),
|
||||||
// Welcome |
|
);
|
||||||
// AppFlowy Editor 🔥!
|
editorState.selection = selection;
|
||||||
// Welcome to AppFlowy Editor 🔥!
|
editorState.insertNewLine();
|
||||||
test('insert new line at the node which contains children', () async {
|
|
||||||
final document = Document.blank().addParagraph(
|
|
||||||
initialText: text,
|
|
||||||
decorator: (index, node) {
|
|
||||||
node.addParagraph(
|
|
||||||
initialText: text,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final editorState = EditorState(document: document);
|
|
||||||
|
|
||||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
||||||
const welcome = 'Welcome ';
|
expect(editorState.getNodeAtPath([0, 0]), null);
|
||||||
final selection = Selection.collapsed(
|
expect(
|
||||||
Position(path: [0], offset: welcome.length),
|
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
||||||
);
|
text.substring(welcome.length),
|
||||||
editorState.selection = selection;
|
);
|
||||||
editorState.insertNewLine();
|
expect(editorState.getNodeAtPath([1, 0])?.delta?.toPlainText(), text);
|
||||||
|
|
||||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
|
||||||
expect(editorState.getNodeAtPath([0, 0]), null);
|
|
||||||
expect(
|
|
||||||
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
|
||||||
text.substring(welcome.length),
|
|
||||||
);
|
|
||||||
expect(editorState.getNodeAtPath([1, 0])?.delta?.toPlainText(), text);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
211
test/new/extensions/node_extension_test.dart
Normal file
211
test/new/extensions/node_extension_test.dart
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../util/util.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
setUpAll(() {
|
||||||
|
if (kDebugMode) {
|
||||||
|
activateLog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() {
|
||||||
|
if (kDebugMode) {
|
||||||
|
deactivateLog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('allSatisfyInSelection - node', () {
|
||||||
|
const welcome = 'Welcome ';
|
||||||
|
const toAppFlowy = 'to AppFlowy';
|
||||||
|
const editor = ' Editor 🔥!';
|
||||||
|
|
||||||
|
// Welcome <b>|to AppFlowy</b> Editor 🔥!
|
||||||
|
test('the selection is collapsed', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(welcome)
|
||||||
|
..insert(
|
||||||
|
toAppFlowy,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..insert(editor),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
// Welcome |to AppFlowy| Editor 🔥!
|
||||||
|
final selection = Selection.collapse(
|
||||||
|
[0],
|
||||||
|
welcome.length,
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
final node = editorState.getNodeAtPath([0]);
|
||||||
|
final result = node!.allSatisfyInSelection(selection, (delta) {
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
return textInserts
|
||||||
|
.every((element) => element.attributes?['bold'] == true);
|
||||||
|
});
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Welcome |<b>to AppFlowy</b>| Editor 🔥!
|
||||||
|
test('the selection is single and not collapsed - 1', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(welcome)
|
||||||
|
..insert(
|
||||||
|
toAppFlowy,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..insert(editor),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
// Welcome |to AppFlowy| Editor 🔥!
|
||||||
|
final selection = Selection.single(
|
||||||
|
path: [0],
|
||||||
|
startOffset: welcome.length,
|
||||||
|
endOffset: welcome.length + toAppFlowy.length,
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
final node = editorState.getNodeAtPath([0]);
|
||||||
|
final result = node!.allSatisfyInSelection(selection, (delta) {
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
return textInserts
|
||||||
|
.every((element) => element.attributes?['bold'] == true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// |Welcome <b>to AppFlowy</b>| Editor 🔥!
|
||||||
|
test('the selection is single and not collapsed - 2', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(welcome)
|
||||||
|
..insert(
|
||||||
|
toAppFlowy,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..insert(editor),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
// Welcome |to AppFlowy| Editor 🔥!
|
||||||
|
final selection = Selection.single(
|
||||||
|
path: [0],
|
||||||
|
startOffset: 0,
|
||||||
|
endOffset: welcome.length + toAppFlowy.length,
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
final node = editorState.getNodeAtPath([0]);
|
||||||
|
final result = node!.allSatisfyInSelection(selection, (delta) {
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
return textInserts
|
||||||
|
.every((element) => element.attributes?['bold'] == true);
|
||||||
|
});
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('allSatisfyInSelection - nodes', () {
|
||||||
|
const welcome = 'Welcome ';
|
||||||
|
const toAppFlowy = 'to AppFlowy';
|
||||||
|
const editor = ' Editor 🔥!';
|
||||||
|
|
||||||
|
// Welcome <b>|to AppFlowy Editor 🔥!</b>
|
||||||
|
// <b>Welcome to AppFlowy|</b> Editor 🔥!
|
||||||
|
test('the selection is not collapsed and not single - 1', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(welcome)
|
||||||
|
..insert(
|
||||||
|
toAppFlowy + editor,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)..addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(
|
||||||
|
welcome + toAppFlowy,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..insert(
|
||||||
|
editor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
// Welcome <b>|to AppFlowy Editor 🔥!</b>
|
||||||
|
// <b>Welcome to AppFlowy|</b> Editor 🔥!
|
||||||
|
final selection = Selection(
|
||||||
|
start: Position(path: [0], offset: welcome.length),
|
||||||
|
end: Position(path: [1], offset: welcome.length + toAppFlowy.length),
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
final nodes = editorState.getNodesInSelection(selection);
|
||||||
|
final result = nodes.allSatisfyInSelection(selection, (delta) {
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
return textInserts
|
||||||
|
.every((element) => element.attributes?['bold'] == true);
|
||||||
|
});
|
||||||
|
expect(result, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
// |Welcome <b>to AppFlowy Editor 🔥!</b>
|
||||||
|
// <b>Welcome to AppFlowy</b> Editor 🔥!|
|
||||||
|
test('the selection is not collapsed and not single - 2', () async {
|
||||||
|
final document = Document.blank().addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(welcome)
|
||||||
|
..insert(
|
||||||
|
toAppFlowy + editor,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)..addParagraph(
|
||||||
|
builder: (index) => Delta()
|
||||||
|
..insert(
|
||||||
|
welcome + toAppFlowy,
|
||||||
|
attributes: {
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
..insert(
|
||||||
|
editor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final editorState = EditorState(document: document);
|
||||||
|
|
||||||
|
// |Welcome <b>to AppFlowy Editor 🔥!</b>
|
||||||
|
// <b>Welcome to AppFlowy</b> Editor 🔥!|
|
||||||
|
final selection = Selection(
|
||||||
|
start: Position(path: [0], offset: 0),
|
||||||
|
end: Position(
|
||||||
|
path: [1],
|
||||||
|
offset: welcome.length + toAppFlowy.length + editor.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
editorState.selection = selection;
|
||||||
|
final nodes = editorState.getNodesInSelection(selection);
|
||||||
|
final result = nodes.allSatisfyInSelection(selection, (delta) {
|
||||||
|
final textInserts = delta.whereType<TextInsert>();
|
||||||
|
return textInserts
|
||||||
|
.every((element) => element.attributes?['bold'] == true);
|
||||||
|
});
|
||||||
|
expect(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ class TestableEditor {
|
||||||
'bulleted_list': BulletedListBlockComponentBuilder(),
|
'bulleted_list': BulletedListBlockComponentBuilder(),
|
||||||
'numbered_list': NumberedListBlockComponentBuilder(),
|
'numbered_list': NumberedListBlockComponentBuilder(),
|
||||||
'quote': QuoteBlockComponentBuilder(),
|
'quote': QuoteBlockComponentBuilder(),
|
||||||
|
'heading': HeadingBlockComponentBuilder(),
|
||||||
},
|
},
|
||||||
characterShortcutEvents: [
|
characterShortcutEvents: [
|
||||||
insertNewLine,
|
insertNewLine,
|
||||||
|
|
@ -39,6 +40,7 @@ class TestableEditor {
|
||||||
formatMinusToBulletedList,
|
formatMinusToBulletedList,
|
||||||
formatNumberToNumberedList,
|
formatNumberToNumberedList,
|
||||||
formatGreaterToQuote,
|
formatGreaterToQuote,
|
||||||
|
formatSignToHeading,
|
||||||
slashCommand,
|
slashCommand,
|
||||||
formatUnderscoreToItalic,
|
formatUnderscoreToItalic,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ extension DocumentExtension on Document {
|
||||||
NodeDecorator? decorator,
|
NodeDecorator? decorator,
|
||||||
}) {
|
}) {
|
||||||
final builder0 = builder ??
|
final builder0 = builder ??
|
||||||
(index) => initialText ?? '🔥 $index. Welcome to AppFlowy Editor!';
|
(index) => Delta()
|
||||||
|
..insert(initialText ?? '🔥 $index. Welcome to AppFlowy Editor!');
|
||||||
final decorator0 = decorator ?? (index, node) {};
|
final decorator0 = decorator ?? (index, node) {};
|
||||||
final children = List.generate(count, (index) {
|
final children = List.generate(count, (index) {
|
||||||
final node = Node(type: 'paragraph');
|
final node = Node(type: 'paragraph');
|
||||||
decorator0(index, node);
|
decorator0(index, node);
|
||||||
node.updateAttributes({
|
node.updateAttributes({
|
||||||
'delta': (Delta()..insert(builder0(index))).toJson(),
|
'delta': builder0(index).toJson(),
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ extension NodeExtension on Node {
|
||||||
NodeDecorator? decorator,
|
NodeDecorator? decorator,
|
||||||
}) {
|
}) {
|
||||||
final builder0 = builder ??
|
final builder0 = builder ??
|
||||||
(index) => initialText ?? '🔥 $index. Welcome to AppFlowy Editor!';
|
(index) => Delta()
|
||||||
|
..insert(initialText ?? '🔥 $index. Welcome to AppFlowy Editor!');
|
||||||
final decorator0 = decorator ?? (index, node) {};
|
final decorator0 = decorator ?? (index, node) {};
|
||||||
final nodes = List.generate(
|
final nodes = List.generate(
|
||||||
count,
|
count,
|
||||||
|
|
@ -18,7 +19,7 @@ extension NodeExtension on Node {
|
||||||
final node = Node(type: 'paragraph');
|
final node = Node(type: 'paragraph');
|
||||||
decorator0(index, node);
|
decorator0(index, node);
|
||||||
node.updateAttributes({
|
node.updateAttributes({
|
||||||
'delta': (Delta()..insert(builder0(index))).toJson(),
|
'delta': builder0(index).toJson(),
|
||||||
});
|
});
|
||||||
return node;
|
return node;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
typedef DeltaBuilder = Delta Function(int index);
|
typedef DeltaBuilder = Delta Function(int index);
|
||||||
|
|
||||||
/// customize the initial text
|
/// customize the initial text
|
||||||
typedef TextBuilder = String Function(int index);
|
typedef TextBuilder = Delta Function(int index);
|
||||||
|
|
||||||
/// customize the node
|
/// customize the node
|
||||||
typedef NodeDecorator = void Function(int index, Node node);
|
typedef NodeDecorator = void Function(int index, Node node);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue