We will use a simple example to illustrate how to quickly add a shortcut event.
In this example, text that starts and ends with an underscore ( _ ) character will be rendered in italics for emphasis. So typing _xxx_
will automatically be converted into xxx.
Let's start with a blank document:
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.topCenter,
child: AppFlowyEditor(
editorState: EditorState.empty(),
editorStyle: EditorStyle.defaultStyle(),
shortcutEvents: const [],
customBuilders: const {},
),
),
);
}
At this point, nothing magic will happen after typing _xxx_
.
To implement our shortcut event we will create a ShortcutEvent
instance to handle an underscore input.
We need to define key
and command
in a ShortCutEvent object to customize hotkeys. We recommend using the description of your event as a key. For example, if the underscore _
is defined to make text italic, the key can be 'Underscore to italic'.
The command, made up of a single keyword such as
underscore
or a combination of keywords using the+
sign in between to concatenate, is a condition that triggers a user-defined function. To see which keywords are available to define a command, please refer to key_mapping.dart. If more than one commands trigger the same handler, then we use ',' to split them. For example, using CTRL and A or CMD and A to 'select all', we describe it ascmd+a,ctrl+a
(case-insensitive).
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
ShortcutEvent underscoreToItalicEvent = ShortcutEvent(
key: 'Underscore to italic',
command: 'shift+underscore',
handler: _underscoreToItalicHandler,
);
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
};
Then, we need to determine if the currently selected node is a TextNode
and if the selection is collapsed.
If so, we will continue.
// ...
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
// Obtain the selection and selected nodes of the current document through the 'selectionService'
// to determine whether the selection is collapsed and whether the selected node is a text node.
final selectionService = editorState.service.selectionService;
final selection = selectionService.currentSelection.value;
final textNodes = selectionService.currentSelectedNodes.whereType<TextNode>();
if (selection == null || !selection.isSingle || textNodes.length != 1) {
return KeyEventResult.ignored;
}
Now, we deal with handling the underscore.
Look for the position of the previous underscore and
// ...
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
// ...
final textNode = textNodes.first;
final text = textNode.toRawString();
// Determine if an 'underscore' already exists in the text node and only once.
final firstUnderscore = text.indexOf('_');
final lastUnderscore = text.lastIndexOf('_');
if (firstUnderscore == -1 ||
firstUnderscore != lastUnderscore ||
firstUnderscore == selection.start.offset - 1) {
return KeyEventResult.ignored;
}
// Delete the previous 'underscore',
// update the style of the text surrounded by the two underscores to 'italic',
// and update the cursor position.
TransactionBuilder(editorState)
..deleteText(textNode, firstUnderscore, 1)
..formatText(
textNode,
firstUnderscore,
selection.end.offset - firstUnderscore - 1,
{
BuiltInAttributeKey.italic: true,
},
)
..afterSelection = Selection.collapsed(
Position(
path: textNode.path,
offset: selection.end.offset - 1,
),
)
..commit();
return KeyEventResult.handled;
};
Now our 'underscore handler' function is done and the only task left is to inject it into the AppFlowyEditor.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.topCenter,
child: AppFlowyEditor(
editorState: EditorState.empty(),
editorStyle: EditorStyle.defaultStyle(),
customBuilders: const {},
shortcutEvents: [
underscoreToItalic,
],
),
),
);
}
Check out the complete code file of this example.
We will use a simple example to show how to quickly add a custom component.
In this example we will render an image from the network.
Let's start with a blank document:
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.topCenter,
child: AppFlowyEditor(
editorState: EditorState.empty(),
editorStyle: EditorStyle.defaultStyle(),
shortcutEvents: const [],
customBuilders: const {},
),
),
);
}
Next, we will choose a unique string for your custom node's type.
We'll use network_image
in this case. And we add network_image_src
to the attributes
to describe the link of the image.
{
"type": "network_image",
"attributes": {
"network_image_src": "https://docs.flutter.dev/assets/images/dash/dash-fainting.gif"
}
}
Then, we create a class that inherits NodeWidgetBuilder. As shown in the autoprompt, we need to implement two functions:
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
throw UnimplementedError();
}
@override
NodeValidator<Node> get nodeValidator => throw UnimplementedError();
}
Now, let's implement a simple image widget based on Image
.
Note that the State
object that is returned by the Widget
must implement Selectable using the with
keyword.
class _NetworkImageNodeWidget extends StatefulWidget {
const _NetworkImageNodeWidget({
Key? key,
required this.node,
}) : super(key: key);
final Node node;
@override
State<_NetworkImageNodeWidget> createState() =>
__NetworkImageNodeWidgetState();
}
class __NetworkImageNodeWidgetState extends State<_NetworkImageNodeWidget>
with Selectable {
RenderBox get _renderBox => context.findRenderObject() as RenderBox;
@override
Widget build(BuildContext context) {
return Image.network(
widget.node.attributes['network_image_src'],
height: 200,
loadingBuilder: (context, child, loadingProgress) =>
loadingProgress == null ? child : const CircularProgressIndicator(),
);
}
@override
Position start() => Position(path: widget.node.path, offset: 0);
@override
Position end() => Position(path: widget.node.path, offset: 1);
@override
Position getPositionInOffset(Offset start) => end();
@override
List<Rect> getRectsInSelection(Selection selection) =>
[Offset.zero & _renderBox.size];
@override
Selection getSelectionInRange(Offset start, Offset end) => Selection.single(
path: widget.node.path,
startOffset: 0,
endOffset: 1,
);
@override
Offset localToGlobal(Offset offset) => _renderBox.localToGlobal(offset);
}
Finally, we return _NetworkImageNodeWidget
in the build
function of NetworkImageNodeWidgetBuilder
...
class NetworkImageNodeWidgetBuilder extends NodeWidgetBuilder {
@override
Widget build(NodeWidgetContext<Node> context) {
return _NetworkImageNodeWidget(
key: context.node.key,
node: context.node,
);
}
@override
NodeValidator<Node> get nodeValidator => (node) {
return node.type == 'network_image' &&
node.attributes['network_image_src'] is String;
};
}
... and register NetworkImageNodeWidgetBuilder
in the AppFlowyEditor
.
final editorState = EditorState(
document: StateTree.empty()
..insert(
[0],
[
TextNode.empty(),
Node.fromJson({
'type': 'network_image',
'attributes': {
'network_image_src':
'https://docs.flutter.dev/assets/images/dash/dash-fainting.gif'
}
})
],
),
);
return AppFlowyEditor(
editorState: editorState,
editorStyle: EditorStyle.defaultStyle(),
shortcutEvents: const [],
customBuilders: {
'network_image': NetworkImageNodeWidgetBuilder(),
},
);
Check out the complete code file of this example.
We will use a simple example to illustrate how to quickly customize a theme.
Let's start with a blank document:
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.topCenter,
child: AppFlowyEditor(
editorState: EditorState.empty(),
editorStyle: EditorStyle.defaultStyle(),
shortcutEvents: const [],
customBuilders: const {},
),
),
);
}
At this point, the editor looks like ...
Next, we will customize the EditorStyle
.
EditorStyle _customizedStyle() {
final editorStyle = EditorStyle.defaultStyle();
return editorStyle.copyWith(
cursorColor: Colors.white,
selectionColor: Colors.blue.withOpacity(0.3),
textStyle: editorStyle.textStyle.copyWith(
defaultTextStyle: GoogleFonts.poppins().copyWith(
color: Colors.white,
fontSize: 14.0,
),
defaultPlaceholderTextStyle: GoogleFonts.poppins().copyWith(
color: Colors.white.withOpacity(0.5),
fontSize: 14.0,
),
bold: const TextStyle(fontWeight: FontWeight.w900),
code: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.red[300],
backgroundColor: Colors.grey.withOpacity(0.3),
),
highlightColorHex: '0x6FFFEB3B',
),
pluginStyles: {
'text/quote': builtInPluginStyle
..update(
'textStyle',
(_) {
return (EditorState editorState, Node node) {
return TextStyle(
color: Colors.blue[200],
fontStyle: FontStyle.italic,
fontSize: 12.0,
);
};
},
),
},
);
}
Now our 'customize style' function is done and the only task left is to inject it into the AppFlowyEditor.
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.topCenter,
child: AppFlowyEditor(
editorState: EditorState.empty(),
editorStyle: _customizedStyle(),
shortcutEvents: const [],
customBuilders: const {},
),
),
);
}