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/util/util.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 {
|
||||
const IconItemWidget({
|
||||
super.key,
|
||||
this.size = const Size.square(28.0),
|
||||
this.size = const Size.square(32.0),
|
||||
required this.iconName,
|
||||
required this.isHighlight,
|
||||
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_widget.dart';
|
||||
|
||||
export 'items/heading_toolbar_item.dart';
|
||||
export 'items/heading_toolbar_items.dart';
|
||||
export 'items/paragraph_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/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:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
extension NodeExtensions on Node {
|
||||
|
|
@ -76,6 +71,26 @@ extension NodeExtensions on Node {
|
|||
}
|
||||
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> {
|
||||
|
|
@ -90,4 +105,39 @@ extension NodesExtensions<T extends Node> on List<T> {
|
|||
|
||||
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,
|
||||
)
|
||||
.addParagraph(
|
||||
builder: (index) => '1.$text',
|
||||
builder: (index) => Delta()..insert('1.$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Future<void> testFormatCharacterShortcut(
|
|||
String text = 'Welcome to AppFlowy Editor 🔥!',
|
||||
}) async {
|
||||
final document = Document.blank().addParagraph(
|
||||
builder: (index) => '$prefix$text',
|
||||
builder: (index) => Delta()..insert('$prefix$text'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,147 +17,151 @@ void main() async {
|
|||
}
|
||||
});
|
||||
|
||||
group('selection_transform.dart', () {
|
||||
group('deleteSelection', () {
|
||||
test('the selection is collapsed', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
group('deleteSelection', () {
|
||||
test('the selection is collapsed', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) =>
|
||||
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [1], offset: 10),
|
||||
);
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [1], offset: 10),
|
||||
);
|
||||
final before = editorState.getNodesInSelection(selection).first;
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
expect(
|
||||
before.toJson(),
|
||||
after.toJson(),
|
||||
);
|
||||
});
|
||||
// nothing happens
|
||||
expect(result, false);
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
expect(
|
||||
before.toJson(),
|
||||
after.toJson(),
|
||||
);
|
||||
});
|
||||
|
||||
test('the selection is single', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
test('the selection is single', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) =>
|
||||
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// |Welcome|
|
||||
final selection = Selection.single(
|
||||
path: [1],
|
||||
startOffset: 3,
|
||||
endOffset: 10,
|
||||
);
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
// |Welcome|
|
||||
final selection = Selection.single(
|
||||
path: [1],
|
||||
startOffset: 3,
|
||||
endOffset: 10,
|
||||
);
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
|
||||
// nothing happens
|
||||
expect(result, true);
|
||||
expect(editorState.selection, selection.collapse(atStart: true));
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
expect(after.delta?.toPlainText(), '1. to AppFlowy Editor 🔥!');
|
||||
});
|
||||
// nothing happens
|
||||
expect(result, true);
|
||||
expect(editorState.selection, selection.collapse(atStart: true));
|
||||
final after = editorState.getNodesInSelection(selection).first;
|
||||
expect(after.delta?.toPlainText(), '1. to AppFlowy Editor 🔥!');
|
||||
});
|
||||
|
||||
test('the selection is not single and not collapsed - 1', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) => '$index. Welcome to AppFlowy Editor 🔥!',
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
test('the selection is not single and not collapsed - 1', () async {
|
||||
final document = Document.blank().addParagraphs(
|
||||
3,
|
||||
builder: (index) =>
|
||||
Delta()..insert('$index. Welcome to AppFlowy Editor 🔥!'),
|
||||
);
|
||||
final editorState = EditorState(document: document);
|
||||
|
||||
// |Welcome
|
||||
// ...
|
||||
// Welcome|
|
||||
final selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 3,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset: 10,
|
||||
),
|
||||
);
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
// |Welcome
|
||||
// ...
|
||||
// Welcome|
|
||||
final selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: 3,
|
||||
),
|
||||
end: Position(
|
||||
path: [2],
|
||||
offset: 10,
|
||||
),
|
||||
);
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
|
||||
// nothing happens
|
||||
expect(result, true);
|
||||
expect(editorState.selection, selection.collapse(atStart: true));
|
||||
final length = editorState.document.root.children.length;
|
||||
expect(length, 1);
|
||||
expect(
|
||||
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
||||
'0. to AppFlowy Editor 🔥!',
|
||||
);
|
||||
});
|
||||
// nothing happens
|
||||
expect(result, true);
|
||||
expect(editorState.selection, selection.collapse(atStart: true));
|
||||
final length = editorState.document.root.children.length;
|
||||
expect(length, 1);
|
||||
expect(
|
||||
editorState.document.nodeAtPath([0])?.delta?.toPlainText(),
|
||||
'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.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) => '$index. Welcome to AppFlowy Editor 🔥!',
|
||||
decorator: (index, node) {
|
||||
node.addParagraphs(
|
||||
2,
|
||||
builder: (index2) =>
|
||||
'$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.0.0 Welcome to AppFlowy Editor 🔥!
|
||||
// 0.1 Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: '0. Welcome '.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0, 0],
|
||||
offset: '0.0. Welcome '.length,
|
||||
),
|
||||
);
|
||||
final result = await editorState.deleteSelection(selection);
|
||||
|
||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||
// 0.0. Welcome |to AppFlowy Editor 🔥!
|
||||
// 0.0.0 Welcome to AppFlowy Editor 🔥!
|
||||
// 0.1 Welcome to AppFlowy Editor 🔥!
|
||||
final selection = Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
offset: '0. Welcome '.length,
|
||||
),
|
||||
end: Position(
|
||||
path: [0, 0],
|
||||
offset: '0.0. Welcome '.length,
|
||||
),
|
||||
);
|
||||
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 🔥!',
|
||||
);
|
||||
});
|
||||
// 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', () {
|
||||
const text = 'Welcome to AppFlowy Editor 🔥!';
|
||||
group('insertNewLine', () {
|
||||
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 🔥!
|
||||
// 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);
|
||||
const welcome = 'Welcome ';
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: welcome.length),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
editorState.insertNewLine();
|
||||
|
||||
// Welcome |to AppFlowy Editor 🔥!
|
||||
const welcome = 'Welcome ';
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: welcome.length),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
editorState.insertNewLine();
|
||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
||||
expect(
|
||||
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
||||
text.substring(welcome.length),
|
||||
);
|
||||
});
|
||||
|
||||
expect(editorState.getNodeAtPath([0])?.delta?.toPlainText(), welcome);
|
||||
expect(
|
||||
editorState.getNodeAtPath([1])?.delta?.toPlainText(),
|
||||
text.substring(welcome.length),
|
||||
);
|
||||
});
|
||||
// Before
|
||||
// Welcome |to AppFlowy Editor 🔥!
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// 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
|
||||
// Welcome |to AppFlowy Editor 🔥!
|
||||
// Welcome to AppFlowy Editor 🔥!
|
||||
// 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);
|
||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||
const welcome = 'Welcome ';
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: welcome.length),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
editorState.insertNewLine();
|
||||
|
||||
// 0. Welcome |to AppFlowy Editor 🔥!
|
||||
const welcome = 'Welcome ';
|
||||
final selection = Selection.collapsed(
|
||||
Position(path: [0], offset: welcome.length),
|
||||
);
|
||||
editorState.selection = selection;
|
||||
editorState.insertNewLine();
|
||||
|
||||
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);
|
||||
});
|
||||
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(),
|
||||
'numbered_list': NumberedListBlockComponentBuilder(),
|
||||
'quote': QuoteBlockComponentBuilder(),
|
||||
'heading': HeadingBlockComponentBuilder(),
|
||||
},
|
||||
characterShortcutEvents: [
|
||||
insertNewLine,
|
||||
|
|
@ -39,6 +40,7 @@ class TestableEditor {
|
|||
formatMinusToBulletedList,
|
||||
formatNumberToNumberedList,
|
||||
formatGreaterToQuote,
|
||||
formatSignToHeading,
|
||||
slashCommand,
|
||||
formatUnderscoreToItalic,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ extension DocumentExtension on Document {
|
|||
NodeDecorator? decorator,
|
||||
}) {
|
||||
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 children = List.generate(count, (index) {
|
||||
final node = Node(type: 'paragraph');
|
||||
decorator0(index, node);
|
||||
node.updateAttributes({
|
||||
'delta': (Delta()..insert(builder0(index))).toJson(),
|
||||
'delta': builder0(index).toJson(),
|
||||
});
|
||||
return node;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ extension NodeExtension on Node {
|
|||
NodeDecorator? decorator,
|
||||
}) {
|
||||
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 nodes = List.generate(
|
||||
count,
|
||||
|
|
@ -18,7 +19,7 @@ extension NodeExtension on Node {
|
|||
final node = Node(type: 'paragraph');
|
||||
decorator0(index, node);
|
||||
node.updateAttributes({
|
||||
'delta': (Delta()..insert(builder0(index))).toJson(),
|
||||
'delta': builder0(index).toJson(),
|
||||
});
|
||||
return node;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
|||
typedef DeltaBuilder = Delta Function(int index);
|
||||
|
||||
/// customize the initial text
|
||||
typedef TextBuilder = String Function(int index);
|
||||
typedef TextBuilder = Delta Function(int index);
|
||||
|
||||
/// customize the node
|
||||
typedef NodeDecorator = void Function(int index, Node node);
|
||||
|
|
|
|||
Loading…
Reference in a new issue