mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #932 from LucasXu0/feat/931
feat: #931 highlight the status of the currently selected style in toolbar
This commit is contained in:
commit
439690f3eb
@ -0,0 +1,8 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension EditorStateExtensions on EditorState {
|
||||
List<TextNode> get selectedTextNodes =>
|
||||
service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
}
|
@ -29,56 +29,63 @@ extension TextNodeExtension on TextNode {
|
||||
}
|
||||
|
||||
bool allSatisfyLinkInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.href, selection, (value) {
|
||||
allSatisfyInSelection(selection, StyleKey.href, (value) {
|
||||
return value != null;
|
||||
});
|
||||
|
||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.bold, selection, (value) {
|
||||
allSatisfyInSelection(selection, StyleKey.bold, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.italic, selection, (value) {
|
||||
allSatisfyInSelection(selection, StyleKey.italic, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.underline, selection, (value) {
|
||||
allSatisfyInSelection(selection, StyleKey.underline, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.strikethrough, selection, (value) {
|
||||
allSatisfyInSelection(selection, StyleKey.strikethrough, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyInSelection(
|
||||
String styleKey,
|
||||
Selection selection,
|
||||
String styleKey,
|
||||
bool Function(dynamic value) test,
|
||||
) {
|
||||
final ops = delta.whereType<TextInsert>();
|
||||
final startOffset =
|
||||
selection.isBackward ? selection.start.offset : selection.end.offset;
|
||||
final endOffset =
|
||||
selection.isBackward ? selection.end.offset : selection.start.offset;
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= endOffset) {
|
||||
break;
|
||||
if (StyleKey.globalStyleKeys.contains(styleKey)) {
|
||||
if (attributes.containsKey(styleKey)) {
|
||||
return test(attributes[styleKey]);
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < endOffset && start + length > startOffset) {
|
||||
if (op.attributes == null ||
|
||||
!op.attributes!.containsKey(styleKey) ||
|
||||
!test(op.attributes![styleKey])) {
|
||||
return false;
|
||||
} else if (StyleKey.partialStyleKeys.contains(styleKey)) {
|
||||
final ops = delta.whereType<TextInsert>();
|
||||
final startOffset =
|
||||
selection.isBackward ? selection.start.offset : selection.end.offset;
|
||||
final endOffset =
|
||||
selection.isBackward ? selection.end.offset : selection.start.offset;
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= endOffset) {
|
||||
break;
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < endOffset && start + length > startOffset) {
|
||||
if (op.attributes == null ||
|
||||
!op.attributes!.containsKey(styleKey) ||
|
||||
!test(op.attributes![styleKey])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
start += length;
|
||||
}
|
||||
start += length;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allNotSatisfyInSelection(
|
||||
@ -111,29 +118,44 @@ extension TextNodeExtension on TextNode {
|
||||
}
|
||||
|
||||
extension TextNodesExtension on List<TextNode> {
|
||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.bold, selection, true);
|
||||
bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
|
||||
selection,
|
||||
StyleKey.bold,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.italic, selection, true);
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
StyleKey.italic,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.underline, selection, true);
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
StyleKey.underline,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.strikethrough, selection, true);
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
StyleKey.strikethrough,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyInSelection(
|
||||
String styleKey,
|
||||
Selection selection,
|
||||
dynamic matchValue,
|
||||
String styleKey,
|
||||
bool Function(dynamic value) test,
|
||||
) {
|
||||
if (isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (length == 1) {
|
||||
return first.allSatisfyInSelection(styleKey, selection, (value) {
|
||||
return value == matchValue;
|
||||
return first.allSatisfyInSelection(selection, styleKey, (value) {
|
||||
return test(value);
|
||||
});
|
||||
} else {
|
||||
for (var i = 0; i < length; i++) {
|
||||
@ -154,8 +176,8 @@ extension TextNodesExtension on List<TextNode> {
|
||||
end: Position(path: node.path, offset: node.toRawString().length),
|
||||
);
|
||||
}
|
||||
if (!node.allSatisfyInSelection(styleKey, newSelection, (value) {
|
||||
return value == matchValue;
|
||||
if (!node.allSatisfyInSelection(newSelection, styleKey, (value) {
|
||||
return test(value);
|
||||
})) {
|
||||
return false;
|
||||
}
|
||||
|
@ -47,6 +47,8 @@ class StyleKey {
|
||||
StyleKey.italic,
|
||||
StyleKey.underline,
|
||||
StyleKey.strikethrough,
|
||||
StyleKey.backgroundColor,
|
||||
StyleKey.href,
|
||||
];
|
||||
|
||||
static List<String> globalStyleKeys = [
|
||||
|
@ -4,38 +4,43 @@ import 'package:appflowy_editor/src/infra/flowy_svg.dart';
|
||||
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
|
||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
|
||||
import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rich_clipboard/rich_clipboard.dart';
|
||||
|
||||
typedef ToolbarEventHandler = void Function(
|
||||
typedef ToolbarItemEventHandler = void Function(
|
||||
EditorState editorState, BuildContext context);
|
||||
typedef ToolbarShowValidator = bool Function(EditorState editorState);
|
||||
typedef ToolbarItemValidator = bool Function(EditorState editorState);
|
||||
typedef ToolbarItemHighlightCallback = bool Function(EditorState editorState);
|
||||
|
||||
class ToolbarItem {
|
||||
ToolbarItem({
|
||||
required this.id,
|
||||
required this.type,
|
||||
required this.icon,
|
||||
required this.iconBuilder,
|
||||
this.tooltipsMessage = '',
|
||||
required this.validator,
|
||||
required this.highlightCallback,
|
||||
required this.handler,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final int type;
|
||||
final Widget icon;
|
||||
final Widget Function(bool isHighlight) iconBuilder;
|
||||
final String tooltipsMessage;
|
||||
final ToolbarShowValidator validator;
|
||||
final ToolbarEventHandler handler;
|
||||
final ToolbarItemValidator validator;
|
||||
final ToolbarItemEventHandler handler;
|
||||
final ToolbarItemHighlightCallback highlightCallback;
|
||||
|
||||
factory ToolbarItem.divider() {
|
||||
return ToolbarItem(
|
||||
id: 'divider',
|
||||
type: -1,
|
||||
icon: const FlowySvg(name: 'toolbar/divider'),
|
||||
iconBuilder: (_) => const FlowySvg(name: 'toolbar/divider'),
|
||||
validator: (editorState) => true,
|
||||
handler: (editorState, context) {},
|
||||
highlightCallback: (editorState) => false,
|
||||
);
|
||||
}
|
||||
|
||||
@ -59,103 +64,205 @@ List<ToolbarItem> defaultToolbarItems = [
|
||||
id: 'appflowy.toolbar.h1',
|
||||
type: 1,
|
||||
tooltipsMessage: 'Heading 1',
|
||||
icon: const FlowySvg(name: 'toolbar/h1'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/h1',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.heading,
|
||||
(value) => value == StyleKey.h1,
|
||||
),
|
||||
handler: (editorState, context) => formatHeading(editorState, StyleKey.h1),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.h2',
|
||||
type: 1,
|
||||
tooltipsMessage: 'Heading 2',
|
||||
icon: const FlowySvg(name: 'toolbar/h2'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/h2',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.heading,
|
||||
(value) => value == StyleKey.h2,
|
||||
),
|
||||
handler: (editorState, context) => formatHeading(editorState, StyleKey.h2),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.h3',
|
||||
type: 1,
|
||||
tooltipsMessage: 'Heading 3',
|
||||
icon: const FlowySvg(name: 'toolbar/h3'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/h3',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.heading,
|
||||
(value) => value == StyleKey.h3,
|
||||
),
|
||||
handler: (editorState, context) => formatHeading(editorState, StyleKey.h3),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.bold',
|
||||
type: 2,
|
||||
tooltipsMessage: 'Bold',
|
||||
icon: const FlowySvg(name: 'toolbar/bold'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/bold',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.bold,
|
||||
(value) => value == true,
|
||||
),
|
||||
handler: (editorState, context) => formatBold(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.italic',
|
||||
type: 2,
|
||||
tooltipsMessage: 'Italic',
|
||||
icon: const FlowySvg(name: 'toolbar/italic'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/italic',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.italic,
|
||||
(value) => value == true,
|
||||
),
|
||||
handler: (editorState, context) => formatItalic(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.underline',
|
||||
type: 2,
|
||||
tooltipsMessage: 'Underline',
|
||||
icon: const FlowySvg(name: 'toolbar/underline'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/underline',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.underline,
|
||||
(value) => value == true,
|
||||
),
|
||||
handler: (editorState, context) => formatUnderline(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.strikethrough',
|
||||
type: 2,
|
||||
tooltipsMessage: 'Strikethrough',
|
||||
icon: const FlowySvg(name: 'toolbar/strikethrough'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/strikethrough',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.strikethrough,
|
||||
(value) => value == true,
|
||||
),
|
||||
handler: (editorState, context) => formatStrikethrough(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.quote',
|
||||
type: 3,
|
||||
tooltipsMessage: 'Quote',
|
||||
icon: const FlowySvg(name: 'toolbar/quote'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/quote',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.subtype,
|
||||
(value) => value == StyleKey.quote,
|
||||
),
|
||||
handler: (editorState, context) => formatQuote(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.bulleted_list',
|
||||
type: 3,
|
||||
tooltipsMessage: 'Bulleted list',
|
||||
icon: const FlowySvg(name: 'toolbar/bulleted_list'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/bulleted_list',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.subtype,
|
||||
(value) => value == StyleKey.bulletedList,
|
||||
),
|
||||
handler: (editorState, context) => formatBulletedList(editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.link',
|
||||
type: 4,
|
||||
tooltipsMessage: 'Link',
|
||||
icon: const FlowySvg(name: 'toolbar/link'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/link',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _onlyShowInSingleTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.href,
|
||||
(value) => value != null,
|
||||
),
|
||||
handler: (editorState, context) => showLinkMenu(context, editorState),
|
||||
),
|
||||
ToolbarItem(
|
||||
id: 'appflowy.toolbar.highlight',
|
||||
type: 4,
|
||||
tooltipsMessage: 'Highlight',
|
||||
icon: const FlowySvg(name: 'toolbar/highlight'),
|
||||
iconBuilder: (isHighlight) => FlowySvg(
|
||||
name: 'toolbar/highlight',
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
),
|
||||
validator: _showInTextSelection,
|
||||
highlightCallback: (editorState) => _allSatisfy(
|
||||
editorState,
|
||||
StyleKey.backgroundColor,
|
||||
(value) => value != null,
|
||||
),
|
||||
handler: (editorState, context) => formatHighlight(editorState),
|
||||
),
|
||||
];
|
||||
|
||||
ToolbarShowValidator _onlyShowInSingleTextSelection = (editorState) {
|
||||
ToolbarItemValidator _onlyShowInSingleTextSelection = (editorState) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
return (nodes.length == 1 && nodes.first is TextNode);
|
||||
};
|
||||
|
||||
ToolbarShowValidator _showInTextSelection = (editorState) {
|
||||
ToolbarItemValidator _showInTextSelection = (editorState) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>();
|
||||
return nodes.isNotEmpty;
|
||||
};
|
||||
|
||||
bool _allSatisfy(
|
||||
EditorState editorState,
|
||||
String styleKey,
|
||||
bool Function(dynamic value) test,
|
||||
) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
return selection != null &&
|
||||
editorState.selectedTextNodes.allSatisfyInSelection(
|
||||
selection,
|
||||
styleKey,
|
||||
test,
|
||||
);
|
||||
}
|
||||
|
||||
OverlayEntry? _linkMenuOverlay;
|
||||
EditorState? _editorState;
|
||||
bool _changeSelectionInner = false;
|
||||
|
@ -6,11 +6,13 @@ class ToolbarItemWidget extends StatelessWidget {
|
||||
const ToolbarItemWidget({
|
||||
Key? key,
|
||||
required this.item,
|
||||
required this.isHighlight,
|
||||
required this.onPressed,
|
||||
}) : super(key: key);
|
||||
|
||||
final ToolbarItem item;
|
||||
final VoidCallback onPressed;
|
||||
final bool isHighlight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -23,8 +25,9 @@ class ToolbarItemWidget extends StatelessWidget {
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: IconButton(
|
||||
highlightColor: Colors.yellow,
|
||||
padding: EdgeInsets.zero,
|
||||
icon: item.icon,
|
||||
icon: item.iconBuilder(isHighlight),
|
||||
iconSize: 28,
|
||||
onPressed: onPressed,
|
||||
),
|
||||
|
@ -64,6 +64,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> with ToolbarMixin {
|
||||
(item) => Center(
|
||||
child: ToolbarItemWidget(
|
||||
item: item,
|
||||
isHighlight: item.highlightCallback(widget.editorState),
|
||||
onPressed: () {
|
||||
item.handler(widget.editorState, context);
|
||||
},
|
||||
|
@ -157,7 +157,7 @@ bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
|
||||
}
|
||||
|
||||
bool _allSatisfyInSelection(
|
||||
EditorState editorState, String styleKey, dynamic value) {
|
||||
EditorState editorState, String styleKey, dynamic matchValue) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||
@ -166,7 +166,9 @@ bool _allSatisfyInSelection(
|
||||
return false;
|
||||
}
|
||||
|
||||
return textNodes.allSatisfyInSelection(styleKey, selection, value);
|
||||
return textNodes.allSatisfyInSelection(selection, styleKey, (value) {
|
||||
return value == matchValue;
|
||||
});
|
||||
}
|
||||
|
||||
bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
|
@ -102,4 +102,9 @@ class _FlowyToolbarState extends State<FlowyToolbar>
|
||||
}
|
||||
return dividedItems;
|
||||
}
|
||||
|
||||
// List<ToolbarItem> _highlightItems(
|
||||
// List<ToolbarItem> items,
|
||||
// Selection selection,
|
||||
// ) {}
|
||||
}
|
||||
|
@ -11,17 +11,28 @@ void main() async {
|
||||
group('toolbar_item_widget.dart', () {
|
||||
testWidgets('test single toolbar item widget', (tester) async {
|
||||
final key = GlobalKey();
|
||||
final iconKey = GlobalKey();
|
||||
var hit = false;
|
||||
final item = ToolbarItem(
|
||||
id: 'appflowy.toolbar.test',
|
||||
type: 1,
|
||||
icon: const Icon(Icons.abc),
|
||||
iconBuilder: (isHighlight) {
|
||||
return Icon(
|
||||
key: iconKey,
|
||||
Icons.abc,
|
||||
color: isHighlight ? Colors.lightBlue : null,
|
||||
);
|
||||
},
|
||||
validator: (editorState) => true,
|
||||
handler: (editorState, context) {},
|
||||
highlightCallback: (editorState) {
|
||||
return true;
|
||||
},
|
||||
);
|
||||
final widget = ToolbarItemWidget(
|
||||
key: key,
|
||||
item: item,
|
||||
isHighlight: true,
|
||||
onPressed: (() {
|
||||
hit = true;
|
||||
}),
|
||||
@ -36,6 +47,11 @@ void main() async {
|
||||
);
|
||||
|
||||
expect(find.byKey(key), findsOneWidget);
|
||||
expect(find.byKey(iconKey), findsOneWidget);
|
||||
expect(
|
||||
(tester.firstWidget(find.byKey(iconKey)) as Icon).color,
|
||||
Colors.lightBlue,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
await tester.pumpAndSettle();
|
||||
|
@ -2,7 +2,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
|
||||
import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -91,8 +90,8 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
var textNode = editor.nodeAtPath([1]) as TextNode;
|
||||
expect(
|
||||
textNode.allSatisfyInSelection(
|
||||
matchStyle,
|
||||
selection,
|
||||
matchStyle,
|
||||
(value) {
|
||||
return value == matchValue;
|
||||
},
|
||||
@ -110,8 +109,8 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
textNode = editor.nodeAtPath([1]) as TextNode;
|
||||
expect(
|
||||
textNode.allSatisfyInSelection(
|
||||
matchStyle,
|
||||
selection,
|
||||
matchStyle,
|
||||
(value) {
|
||||
return value == matchValue;
|
||||
},
|
||||
@ -144,12 +143,12 @@ Future<void> _testUpdateTextStyleByCommandX(
|
||||
for (final node in nodes) {
|
||||
expect(
|
||||
node.allSatisfyInSelection(
|
||||
matchStyle,
|
||||
Selection.single(
|
||||
path: node.path,
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
matchStyle,
|
||||
(value) {
|
||||
return value == matchValue;
|
||||
},
|
||||
@ -196,11 +195,6 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
// show toolbar
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
|
||||
final item = defaultToolbarItems
|
||||
.where((item) => item.id == 'appflowy.toolbar.link')
|
||||
.first;
|
||||
expect(find.byWidget(item.icon), findsOneWidget);
|
||||
|
||||
// trigger the link menu
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.keyK, isMetaPressed: true);
|
||||
|
||||
@ -215,8 +209,8 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
final node = editor.nodeAtPath([1]) as TextNode;
|
||||
expect(
|
||||
node.allSatisfyInSelection(
|
||||
StyleKey.href,
|
||||
selection,
|
||||
StyleKey.href,
|
||||
(value) => value == link,
|
||||
),
|
||||
true);
|
||||
@ -244,8 +238,8 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
|
||||
|
||||
expect(
|
||||
node.allSatisfyInSelection(
|
||||
StyleKey.href,
|
||||
selection,
|
||||
StyleKey.href,
|
||||
(value) => value == link,
|
||||
),
|
||||
false);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import '../infra/test_editor.dart';
|
||||
@ -30,7 +32,136 @@ void main() async {
|
||||
final item = defaultToolbarItems
|
||||
.where((item) => item.id == 'appflowy.toolbar.link')
|
||||
.first;
|
||||
expect(find.byWidget(item.icon), findsNothing);
|
||||
final finder = find.byType(ToolbarItemWidget);
|
||||
|
||||
expect(
|
||||
tester
|
||||
.widgetList<ToolbarItemWidget>(finder)
|
||||
.toList(growable: false)
|
||||
.where((element) => element.item.id == item.id)
|
||||
.isEmpty,
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Test toolbar service in single text selection with StyleKey.partialStyleKeys',
|
||||
(tester) async {
|
||||
final attributes = StyleKey.partialStyleKeys.fold<Attributes>({},
|
||||
(previousValue, element) {
|
||||
if (element == StyleKey.backgroundColor) {
|
||||
previousValue[element] = '0x6000BCF0';
|
||||
} else if (element == StyleKey.href) {
|
||||
previousValue[element] = 'appflowy.io';
|
||||
} else {
|
||||
previousValue[element] = true;
|
||||
}
|
||||
return previousValue;
|
||||
});
|
||||
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text)
|
||||
..insertTextNode(
|
||||
null,
|
||||
delta: Delta([
|
||||
TextInsert(text),
|
||||
TextInsert(text, attributes),
|
||||
TextInsert(text),
|
||||
]),
|
||||
);
|
||||
await editor.startTesting();
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: text.length),
|
||||
);
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
|
||||
void testHighlight(bool expectedValue) {
|
||||
for (final styleKey in StyleKey.partialStyleKeys) {
|
||||
var key = styleKey;
|
||||
if (styleKey == StyleKey.backgroundColor) {
|
||||
key = 'highlight';
|
||||
} else if (styleKey == StyleKey.href) {
|
||||
key = 'link';
|
||||
}
|
||||
final itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.$key');
|
||||
expect(itemWidget.isHighlight, expectedValue);
|
||||
}
|
||||
}
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0, endOffset: text.length * 2),
|
||||
);
|
||||
testHighlight(false);
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [1],
|
||||
startOffset: text.length,
|
||||
endOffset: text.length * 2,
|
||||
),
|
||||
);
|
||||
testHighlight(true);
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [1],
|
||||
startOffset: text.length + 2,
|
||||
endOffset: text.length * 2 - 2,
|
||||
),
|
||||
);
|
||||
testHighlight(true);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'Test toolbar service in single text selection with StyleKey.globalStyleKeys',
|
||||
(tester) async {
|
||||
const text = 'Welcome to Appflowy 😁';
|
||||
|
||||
final editor = tester.editor
|
||||
..insertTextNode(text, attributes: {
|
||||
StyleKey.subtype: StyleKey.heading,
|
||||
StyleKey.heading: StyleKey.h1,
|
||||
})
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {StyleKey.subtype: StyleKey.quote},
|
||||
)
|
||||
..insertTextNode(
|
||||
text,
|
||||
attributes: {StyleKey.subtype: StyleKey.bulletedList},
|
||||
);
|
||||
await editor.startTesting();
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0, endOffset: text.length),
|
||||
);
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
var itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.h1');
|
||||
expect(itemWidget.isHighlight, true);
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [1], startOffset: 0, endOffset: text.length),
|
||||
);
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.quote');
|
||||
expect(itemWidget.isHighlight, true);
|
||||
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [2], startOffset: 0, endOffset: text.length),
|
||||
);
|
||||
expect(find.byType(ToolbarWidget), findsOneWidget);
|
||||
itemWidget = _itemWidgetForId(tester, 'appflowy.toolbar.bulleted_list');
|
||||
expect(itemWidget.isHighlight, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ToolbarItemWidget _itemWidgetForId(WidgetTester tester, String id) {
|
||||
final finder = find.byType(ToolbarItemWidget);
|
||||
final itemWidgets = tester
|
||||
.widgetList<ToolbarItemWidget>(finder)
|
||||
.where((element) => element.item.id == id);
|
||||
expect(itemWidgets.length, 1);
|
||||
return itemWidgets.first;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user