mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1166 from LucasXu0/code_block
Implement code block feature for appflowy editor
This commit is contained in:
commit
b578067092
@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/plugin/code_block_node_widget.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -116,9 +117,17 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
editorState: _editorState!,
|
||||
editorStyle: _editorStyle,
|
||||
editable: true,
|
||||
customBuilders: {
|
||||
'text/code_block': CodeBlockNodeWidgetBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
enterInCodeBlock,
|
||||
ignoreKeysInCodeBlock,
|
||||
underscoreToItalic,
|
||||
],
|
||||
selectionMenuItems: [
|
||||
codeBlockItem,
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
|
@ -0,0 +1,277 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:highlight/highlight.dart' as highlight;
|
||||
import 'package:highlight/languages/all.dart';
|
||||
|
||||
ShortcutEvent enterInCodeBlock = ShortcutEvent(
|
||||
key: 'Enter in code block',
|
||||
command: 'enter',
|
||||
handler: _enterInCodeBlockHandler,
|
||||
);
|
||||
|
||||
ShortcutEvent ignoreKeysInCodeBlock = ShortcutEvent(
|
||||
key: 'White space in code block',
|
||||
command: 'space,slash,shift+underscore',
|
||||
handler: _ignorekHandler,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _enterInCodeBlockHandler = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final codeBlockNode =
|
||||
nodes.whereType<TextNode>().where((node) => node.id == 'text/code_block');
|
||||
if (codeBlockNode.length != 1 || selection == null) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (selection.isCollapsed) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertText(codeBlockNode.first, selection.end.offset, '\n')
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
ShortcutEventHandler _ignorekHandler = (editorState, event) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final codeBlockNodes =
|
||||
nodes.whereType<TextNode>().where((node) => node.id == 'text/code_block');
|
||||
if (codeBlockNodes.length == 1) {
|
||||
return KeyEventResult.skipRemainingHandlers;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
SelectionMenuItem codeBlockItem = SelectionMenuItem(
|
||||
name: 'Code Block',
|
||||
icon: const Icon(Icons.abc),
|
||||
keywords: ['code block'],
|
||||
handler: (editorState, _, __) {
|
||||
final selection =
|
||||
editorState.service.selectionService.currentSelection.value;
|
||||
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (textNodes.first.toRawString().isEmpty) {
|
||||
TransactionBuilder(editorState)
|
||||
..updateNode(textNodes.first, {
|
||||
'subtype': 'code_block',
|
||||
'theme': 'vs',
|
||||
'language': null,
|
||||
})
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
} else {
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
selection.end.path.next,
|
||||
TextNode(
|
||||
type: 'text',
|
||||
children: LinkedList(),
|
||||
attributes: {
|
||||
'subtype': 'code_block',
|
||||
'theme': 'vs',
|
||||
'language': null,
|
||||
},
|
||||
delta: Delta()..insert('\n'),
|
||||
),
|
||||
)
|
||||
..afterSelection = selection
|
||||
..commit();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
class CodeBlockNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<TextNode> context) {
|
||||
return _CodeBlockNodeWidge(
|
||||
key: context.node.key,
|
||||
textNode: context.node,
|
||||
editorState: context.editorState,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => (node) {
|
||||
return node is TextNode && node.attributes['theme'] is String;
|
||||
};
|
||||
}
|
||||
|
||||
class _CodeBlockNodeWidge extends StatefulWidget {
|
||||
const _CodeBlockNodeWidge({
|
||||
Key? key,
|
||||
required this.textNode,
|
||||
required this.editorState,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextNode textNode;
|
||||
final EditorState editorState;
|
||||
|
||||
@override
|
||||
State<_CodeBlockNodeWidge> createState() => __CodeBlockNodeWidgeState();
|
||||
}
|
||||
|
||||
class __CodeBlockNodeWidgeState extends State<_CodeBlockNodeWidge>
|
||||
with SelectableMixin, DefaultSelectable {
|
||||
final _richTextKey = GlobalKey(debugLabel: 'code_block_text');
|
||||
final _padding = const EdgeInsets.only(left: 20, top: 20, bottom: 20);
|
||||
String? get _language => widget.textNode.attributes['language'] as String?;
|
||||
String? _detectLanguage;
|
||||
|
||||
@override
|
||||
SelectableMixin<StatefulWidget> get forward =>
|
||||
_richTextKey.currentState as SelectableMixin;
|
||||
|
||||
@override
|
||||
GlobalKey<State<StatefulWidget>>? get iconKey => null;
|
||||
|
||||
@override
|
||||
Offset get baseOffset => super.baseOffset + _padding.topLeft;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildCodeBlock(context),
|
||||
_buildSwitchCodeButton(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCodeBlock(BuildContext context) {
|
||||
final result = highlight.highlight.parse(
|
||||
widget.textNode.toRawString(),
|
||||
language: _language,
|
||||
autoDetection: _language == null,
|
||||
);
|
||||
_detectLanguage = _language ?? result.language;
|
||||
final code = result.nodes;
|
||||
final codeTextSpan = _convert(code!);
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
),
|
||||
padding: _padding,
|
||||
width: MediaQuery.of(context).size.width,
|
||||
child: FlowyRichText(
|
||||
key: _richTextKey,
|
||||
textNode: widget.textNode,
|
||||
editorState: widget.editorState,
|
||||
textSpanDecorator: (textSpan) => TextSpan(
|
||||
style: widget.editorState.editorStyle.textStyle.defaultTextStyle,
|
||||
children: codeTextSpan,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSwitchCodeButton(BuildContext context) {
|
||||
return Positioned(
|
||||
top: -5,
|
||||
right: 0,
|
||||
child: DropdownButton<String>(
|
||||
value: _detectLanguage,
|
||||
onChanged: (value) {
|
||||
TransactionBuilder(widget.editorState)
|
||||
..updateNode(widget.textNode, {
|
||||
'language': value,
|
||||
})
|
||||
..commit();
|
||||
},
|
||||
items: allLanguages.keys.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 12.0),
|
||||
),
|
||||
);
|
||||
}).toList(growable: false),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Copy from flutter.highlight package.
|
||||
// https://github.com/git-touch/highlight.dart/blob/master/flutter_highlight/lib/flutter_highlight.dart
|
||||
List<TextSpan> _convert(List<highlight.Node> nodes) {
|
||||
List<TextSpan> spans = [];
|
||||
var currentSpans = spans;
|
||||
List<List<TextSpan>> stack = [];
|
||||
|
||||
_traverse(highlight.Node node) {
|
||||
if (node.value != null) {
|
||||
currentSpans.add(node.className == null
|
||||
? TextSpan(text: node.value)
|
||||
: TextSpan(
|
||||
text: node.value,
|
||||
style: _builtInCodeBlockTheme[node.className!]));
|
||||
} else if (node.children != null) {
|
||||
List<TextSpan> tmp = [];
|
||||
currentSpans.add(TextSpan(
|
||||
children: tmp, style: _builtInCodeBlockTheme[node.className!]));
|
||||
stack.add(currentSpans);
|
||||
currentSpans = tmp;
|
||||
|
||||
for (var n in node.children!) {
|
||||
_traverse(n);
|
||||
if (n == node.children!.last) {
|
||||
currentSpans = stack.isEmpty ? spans : stack.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var node in nodes) {
|
||||
_traverse(node);
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
}
|
||||
|
||||
const _builtInCodeBlockTheme = {
|
||||
'root':
|
||||
TextStyle(backgroundColor: Color(0xffffffff), color: Color(0xff000000)),
|
||||
'comment': TextStyle(color: Color(0xff007400)),
|
||||
'quote': TextStyle(color: Color(0xff007400)),
|
||||
'tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'attribute': TextStyle(color: Color(0xffaa0d91)),
|
||||
'keyword': TextStyle(color: Color(0xffaa0d91)),
|
||||
'selector-tag': TextStyle(color: Color(0xffaa0d91)),
|
||||
'literal': TextStyle(color: Color(0xffaa0d91)),
|
||||
'name': TextStyle(color: Color(0xffaa0d91)),
|
||||
'variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'template-variable': TextStyle(color: Color(0xff3F6E74)),
|
||||
'code': TextStyle(color: Color(0xffc41a16)),
|
||||
'string': TextStyle(color: Color(0xffc41a16)),
|
||||
'meta-string': TextStyle(color: Color(0xffc41a16)),
|
||||
'regexp': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'link': TextStyle(color: Color(0xff0E0EFF)),
|
||||
'title': TextStyle(color: Color(0xff1c00cf)),
|
||||
'symbol': TextStyle(color: Color(0xff1c00cf)),
|
||||
'bullet': TextStyle(color: Color(0xff1c00cf)),
|
||||
'number': TextStyle(color: Color(0xff1c00cf)),
|
||||
'section': TextStyle(color: Color(0xff643820)),
|
||||
'meta': TextStyle(color: Color(0xff643820)),
|
||||
'type': TextStyle(color: Color(0xff5c2699)),
|
||||
'built_in': TextStyle(color: Color(0xff5c2699)),
|
||||
'builtin-name': TextStyle(color: Color(0xff5c2699)),
|
||||
'params': TextStyle(color: Color(0xff5c2699)),
|
||||
'attr': TextStyle(color: Color(0xff836C28)),
|
||||
'subst': TextStyle(color: Color(0xff000000)),
|
||||
'formula': TextStyle(
|
||||
backgroundColor: Color(0xffeeeeee), fontStyle: FontStyle.italic),
|
||||
'addition': TextStyle(backgroundColor: Color(0xffbaeeba)),
|
||||
'deletion': TextStyle(backgroundColor: Color(0xffffc8bd)),
|
||||
'selector-id': TextStyle(color: Color(0xff9b703f)),
|
||||
'selector-class': TextStyle(color: Color(0xff9b703f)),
|
||||
'doctag': TextStyle(fontWeight: FontWeight.bold),
|
||||
'strong': TextStyle(fontWeight: FontWeight.bold),
|
||||
'emphasis': TextStyle(fontStyle: FontStyle.italic),
|
||||
};
|
@ -43,6 +43,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
file_picker: ^5.0.1
|
||||
universal_html: ^2.0.8
|
||||
highlight: ^0.7.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -28,4 +28,8 @@ export 'src/service/shortcut_event/keybinding.dart';
|
||||
export 'src/service/shortcut_event/shortcut_event.dart';
|
||||
export 'src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
export 'src/extensions/attributes_extension.dart';
|
||||
export 'src/extensions/path_extensions.dart';
|
||||
export 'src/render/rich_text/default_selectable.dart';
|
||||
export 'src/render/rich_text/flowy_rich_text.dart';
|
||||
export 'src/render/selection_menu/selection_menu_widget.dart';
|
||||
export 'src/l10n/l10n.dart';
|
||||
|
@ -93,12 +93,14 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
}
|
||||
|
||||
void updateAttributes(Attributes attributes) {
|
||||
bool shouldNotifyParent = _attributes['subtype'] != attributes['subtype'];
|
||||
|
||||
final oldAttributes = {..._attributes};
|
||||
_attributes = composeAttributes(_attributes, attributes) ?? {};
|
||||
|
||||
// Notifies the new attributes
|
||||
// if attributes contains 'subtype', should notify parent to rebuild node
|
||||
// else, just notify current node.
|
||||
bool shouldNotifyParent =
|
||||
_attributes['subtype'] != oldAttributes['subtype'];
|
||||
shouldNotifyParent ? parent?.notifyListeners() : notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,6 @@ import 'package:appflowy_editor/src/service/default_text_operations/format_rich_
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
|
||||
typedef ToolbarItemEventHandler = void Function(
|
||||
EditorState editorState, BuildContext context);
|
||||
@ -120,7 +119,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/bold',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.bold,
|
||||
@ -136,7 +135,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/italic',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.italic,
|
||||
@ -152,7 +151,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/underline',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.underline,
|
||||
@ -168,7 +167,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/strikethrough',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.strikethrough,
|
||||
@ -184,7 +183,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/code',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.code,
|
||||
@ -248,7 +247,7 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
name: 'toolbar/highlight',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
validator: _showInBuiltInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
BuiltInAttributeKey.backgroundColor,
|
||||
@ -262,13 +261,22 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
];
|
||||
|
||||
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
||||
final result = _showInBuiltInTextSelection(editorState);
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
return (nodes.length == 1 && nodes.first is TextNode);
|
||||
};
|
||||
|
||||
ToolbarItemValidator _showInTextSelection = (editorState) {
|
||||
ToolbarItemValidator _showInBuiltInTextSelection = (editorState) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
.whereType<TextNode>()
|
||||
.where(
|
||||
(textNode) =>
|
||||
BuiltInAttributeKey.globalStyleKeys.contains(textNode.subtype) ||
|
||||
textNode.subtype == null,
|
||||
);
|
||||
return nodes.isNotEmpty;
|
||||
};
|
||||
|
||||
|
@ -118,8 +118,8 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
|
||||
key: editorState.service.keyboardServiceKey,
|
||||
editable: widget.editable,
|
||||
shortcutEvents: [
|
||||
...builtInShortcutEvents,
|
||||
...widget.shortcutEvents,
|
||||
...builtInShortcutEvents,
|
||||
],
|
||||
editorState: editorState,
|
||||
child: FlowyToolbar(
|
||||
|
@ -117,12 +117,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
makeFollowingNodesIncremental(editorState, insertPath, afterSelection,
|
||||
beginNum: prevNumber);
|
||||
} else {
|
||||
bool needCopyAttributes = ![
|
||||
BuiltInAttributeKey.heading,
|
||||
BuiltInAttributeKey.quote,
|
||||
].contains(subtype);
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta(),
|
||||
attributes: needCopyAttributes ? null : {},
|
||||
),
|
||||
)
|
||||
..afterSelection = afterSelection
|
||||
@ -173,7 +178,9 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
|
||||
Attributes _attributesFromPreviousLine(TextNode textNode) {
|
||||
final prevAttributes = textNode.attributes;
|
||||
final subType = textNode.subtype;
|
||||
if (subType == null || subType == BuiltInAttributeKey.heading) {
|
||||
if (subType == null ||
|
||||
subType == BuiltInAttributeKey.heading ||
|
||||
subType == BuiltInAttributeKey.quote) {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
@ -13,12 +13,19 @@ ShortcutEventHandler tabHandler = (editorState, event) {
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final previous = textNode.previous;
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList ||
|
||||
previous == null ||
|
||||
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
|
||||
if (textNode.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertText(textNode, selection.end.offset, ' ' * 4)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
|
||||
if (previous == null ||
|
||||
previous.subtype != BuiltInAttributeKey.bulletedList) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
final path = previous.path + [previous.children.length];
|
||||
final afterSelection = Selection(
|
||||
start: selection.start.copyWith(path: path),
|
||||
|
@ -124,6 +124,8 @@ class _AppFlowyKeyboardState extends State<AppFlowyKeyboard>
|
||||
final result = shortcutEvent.handler(widget.editorState, event);
|
||||
if (result == KeyEventResult.handled) {
|
||||
return KeyEventResult.handled;
|
||||
} else if (result == KeyEventResult.skipRemainingHandlers) {
|
||||
return KeyEventResult.skipRemainingHandlers;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
@ -38,14 +38,17 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
||||
@override
|
||||
void showInOffset(Offset offset, LayerLink layerLink) {
|
||||
hide();
|
||||
|
||||
final items = _filterItems(defaultToolbarItems);
|
||||
if (items.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_toolbarOverlay = OverlayEntry(
|
||||
builder: (context) => ToolbarWidget(
|
||||
key: _toolbarWidgetKey,
|
||||
editorState: widget.editorState,
|
||||
layerLink: layerLink,
|
||||
offset: offset,
|
||||
items: _filterItems(defaultToolbarItems),
|
||||
items: items,
|
||||
),
|
||||
);
|
||||
Overlay.of(context)?.insert(_toolbarOverlay!);
|
||||
@ -102,9 +105,4 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
||||
}
|
||||
return dividedItems;
|
||||
}
|
||||
|
||||
// List<ToolbarItem> _highlightItems(
|
||||
// List<ToolbarItem> items,
|
||||
// Selection selection,
|
||||
// ) {}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../../infra/test_editor.dart';
|
||||
import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
|
||||
|
||||
void main() async {
|
||||
setUpAll(() {
|
||||
@ -171,13 +170,27 @@ Future<void> _testStyleNeedToBeCopy(WidgetTester tester, String style) async {
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
||||
expect(editor.nodeAtPath([4])?.subtype, style);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
expect(editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
||||
expect(editor.nodeAtPath([4])?.subtype, null);
|
||||
if ([BuiltInAttributeKey.heading, BuiltInAttributeKey.quote]
|
||||
.contains(style)) {
|
||||
expect(editor.nodeAtPath([4])?.subtype, null);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection, Selection.single(path: [5], startOffset: 0));
|
||||
expect(editor.nodeAtPath([5])?.subtype, null);
|
||||
} else {
|
||||
expect(editor.nodeAtPath([4])?.subtype, style);
|
||||
|
||||
await editor.pressLogicKey(
|
||||
LogicalKeyboardKey.enter,
|
||||
);
|
||||
expect(
|
||||
editor.documentSelection, Selection.single(path: [4], startOffset: 0));
|
||||
expect(editor.nodeAtPath([4])?.subtype, null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _testMultipleSelection(
|
||||
|
@ -15,23 +15,24 @@ void main() async {
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
final document = editor.document;
|
||||
|
||||
var selection = Selection.single(path: [0], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0], startOffset: 4),
|
||||
);
|
||||
|
||||
selection = Selection.single(path: [1], startOffset: 0);
|
||||
await editor.updateSelection(selection);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [1], startOffset: 4),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('press tab in bulleted list', (tester) async {
|
||||
@ -63,7 +64,10 @@ void main() async {
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.tab);
|
||||
|
||||
// nothing happens
|
||||
expect(editor.documentSelection, selection);
|
||||
expect(
|
||||
editor.documentSelection,
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
expect(editor.document.toJson(), document.toJson());
|
||||
|
||||
// Before
|
||||
|
Loading…
Reference in New Issue
Block a user