fix: convert markdown symbol to styled text fails (#1090)
* fix: convert markdown symbol to styled text fails * docs: update customzing.md * chore: disable macOS titlebar * chore: specify the built-in language as en_US * docs: update documentation * docs: update documentation
@ -27,7 +27,7 @@ and the Flutter guide for
|
||||
</p>
|
||||
|
||||
<div align="center">
|
||||
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy-editor-example.gif?raw=true" width = "700" style = "padding: 100"/>
|
||||
<img src="https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/images/appflowy_editor_example.mp4?raw=true" width = "700" style = "padding: 100"/>
|
||||
</div>
|
||||
|
||||
## Key Features
|
||||
@ -58,8 +58,6 @@ final editorStyle = EditorStyle.defaultStyle();
|
||||
final editorState = EditorState.empty(); // an empty state
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
shortcutEvents: const [],
|
||||
customBuilders: const {},
|
||||
editorStyle: editorStyle,
|
||||
);
|
||||
```
|
||||
@ -72,8 +70,6 @@ final editorStyle = EditorStyle.defaultStyle();
|
||||
final editorState = EditorState(StateTree.fromJson(data));
|
||||
final editor = AppFlowyEditor(
|
||||
editorState: editorState,
|
||||
shortcutEvents: const [],
|
||||
customBuilders: const {},
|
||||
editorStyle: editorStyle,
|
||||
);
|
||||
```
|
||||
@ -113,7 +109,7 @@ Please refer to our documentation on customizing AppFlowy for a detailed discuss
|
||||
|
||||
Below are some examples of shortcut event customizations:
|
||||
|
||||
* [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys
|
||||
* [BIUS](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys
|
||||
* [Paste HTML](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart) gives you an idea on how to handle pasted styles through shortcut keys
|
||||
* Need more examples? Check out [Internal key event handlers](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers)
|
||||
|
||||
|
@ -27,7 +27,7 @@ Widget build(BuildContext context) {
|
||||
|
||||
At this point, nothing magic will happen after typing `_xxx_`.
|
||||
|
||||
![Before](./images/customizing_a_shortcut_event_before.gif)
|
||||
![Before](./images/customize_a_shortcut_event_before.gif)
|
||||
|
||||
To implement our shortcut event we will create a `ShortcutEvent` instance to handle an underscore input.
|
||||
|
||||
@ -39,11 +39,10 @@ We need to define `key` and `command` in a ShortCutEvent object to customize hot
|
||||
```dart
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
ShortcutEvent underscoreToItalicEvent = ShortcutEvent(
|
||||
key: 'Underscore to italic',
|
||||
command: 'underscore',
|
||||
command: 'shift+underscore',
|
||||
handler: _underscoreToItalicHandler,
|
||||
);
|
||||
|
||||
@ -82,9 +81,12 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final text = textNode.toRawString();
|
||||
// Determine if an 'underscore' already exists in the text node
|
||||
final previousUnderscore = text.indexOf('_');
|
||||
if (previousUnderscore == -1) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
@ -92,15 +94,20 @@ ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
// update the style of the text surrounded by the two underscores to 'italic',
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(textNode, previousUnderscore, 1)
|
||||
..deleteText(textNode, firstUnderscore, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
previousUnderscore,
|
||||
selection.end.offset - previousUnderscore - 1,
|
||||
{'italic': true},
|
||||
firstUnderscore,
|
||||
selection.end.offset - firstUnderscore - 1,
|
||||
{
|
||||
BuiltInAttributeKey.italic: true,
|
||||
},
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: selection.end.offset - 1),
|
||||
Position(
|
||||
path: textNode.path,
|
||||
offset: selection.end.offset - 1,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
|
||||
@ -121,7 +128,7 @@ Widget build(BuildContext context) {
|
||||
editorStyle: EditorStyle.defaultStyle(),
|
||||
customBuilders: const {},
|
||||
shortcutEvents: [
|
||||
_underscoreToItalicHandler,
|
||||
underscoreToItalic,
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -129,9 +136,9 @@ Widget build(BuildContext context) {
|
||||
}
|
||||
```
|
||||
|
||||
![After](./images/customizing_a_shortcut_event_after.gif)
|
||||
![After](./images/customize_a_shortcut_event_after.gif)
|
||||
|
||||
Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic_key_event_handler.dart) file of this example.
|
||||
Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/underscore_to_italic.dart) file of this example.
|
||||
|
||||
|
||||
## Customizing a Component
|
||||
@ -297,7 +304,7 @@ return AppFlowyEditor(
|
||||
);
|
||||
```
|
||||
|
||||
![Whew!](./images/customizing_a_component.gif)
|
||||
![Whew!](./images/customize_a_component.gif)
|
||||
|
||||
Check out the [complete code](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) file of this example.
|
||||
|
||||
|
Before Width: | Height: | Size: 998 KiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 367 KiB |
After Width: | Height: | Size: 358 KiB |
Before Width: | Height: | Size: 4.2 MiB |
Before Width: | Height: | Size: 101 KiB |
Before Width: | Height: | Size: 84 KiB |
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:example/plugin/underscore_to_italic_key_event_handler.dart';
|
||||
import 'package:example/plugin/underscore_to_italic.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
@ -29,7 +29,7 @@ class MyApp extends StatelessWidget {
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
AppFlowyEditorLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
|
||||
supportedLocales: const [Locale('en', 'US')],
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
@ -113,7 +113,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
editorState: _editorState!,
|
||||
editorStyle: _editorStyle,
|
||||
shortcutEvents: [
|
||||
underscoreToItalicEvent,
|
||||
underscoreToItalic,
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -186,6 +186,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
final path = directory.path;
|
||||
final file = File('$path/editor.json');
|
||||
setState(() {
|
||||
_editorState = null;
|
||||
_jsonString = file.readAsString();
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,53 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEvent underscoreToItalic = ShortcutEvent(
|
||||
key: 'Underscore to italic',
|
||||
command: 'shift+underscore',
|
||||
handler: _underscoreToItalicHandler,
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEvent underscoreToItalicEvent = ShortcutEvent(
|
||||
key: 'Underscore to italic',
|
||||
command: 'underscore',
|
||||
handler: _underscoreToItalicHandler,
|
||||
);
|
||||
|
||||
ShortcutEventHandler _underscoreToItalicHandler = (editorState, event) {
|
||||
// Obtaining the selection and selected nodes of the current document through `selectionService`,
|
||||
// and determine whether it is a single selection 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;
|
||||
}
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final text = textNode.toRawString();
|
||||
// Determine if `underscore` already exists in the text node
|
||||
final previousUnderscore = text.indexOf('_');
|
||||
if (previousUnderscore == -1) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
// Delete the previous `underscore`,
|
||||
// update the style of the text surrounded by two underscores to `italic`,
|
||||
// and update the cursor position.
|
||||
TransactionBuilder(editorState)
|
||||
..deleteText(textNode, previousUnderscore, 1)
|
||||
..formatText(
|
||||
textNode,
|
||||
previousUnderscore,
|
||||
selection.end.offset - previousUnderscore - 1,
|
||||
{'italic': true},
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(path: textNode.path, offset: selection.end.offset - 1),
|
||||
)
|
||||
..commit();
|
||||
|
||||
return KeyEventResult.handled;
|
||||
};
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21225" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21225"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
@ -13,7 +13,7 @@
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target">
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="example" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/>
|
||||
<outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/>
|
||||
@ -330,14 +330,15 @@
|
||||
</items>
|
||||
<point key="canvasLocation" x="142" y="-258"/>
|
||||
</menu>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" titlebarAppearsTransparent="YES" titleVisibility="hidden" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="example" customModuleProvider="target">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES" fullSizeContentView="YES"/>
|
||||
<rect key="contentRect" x="335" y="390" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1920" height="1055"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
<point key="canvasLocation" x="126" y="-658"/>
|
||||
</window>
|
||||
</objects>
|
||||
</document>
|
||||
|
@ -10,6 +10,7 @@ export 'src/document/selection.dart';
|
||||
export 'src/document/state_tree.dart';
|
||||
export 'src/document/text_delta.dart';
|
||||
export 'src/document/attributes.dart';
|
||||
export 'src/document/built_in_attribute_keys.dart';
|
||||
export 'src/editor_state.dart';
|
||||
export 'src/operation/operation.dart';
|
||||
export 'src/operation/transaction.dart';
|
||||
|
@ -44,10 +44,17 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
|
||||
}
|
||||
|
||||
final textNode = textNodes.first;
|
||||
final text = textNode.toRawString();
|
||||
final text = textNode.toRawString().substring(0, selection.end.offset);
|
||||
|
||||
final numberMatch = _numberRegex.firstMatch(text);
|
||||
if (numberMatch != null) {
|
||||
|
||||
if ((_checkboxListSymbols + _unCheckboxListSymbols).contains(text)) {
|
||||
return _toCheckboxList(editorState, textNode);
|
||||
} else if (_bulletedListSymbols.contains(text)) {
|
||||
return _toBulletedList(editorState, textNode);
|
||||
} else if (_countOfSign(text, selection) != 0) {
|
||||
return _toHeadingStyle(editorState, textNode, selection);
|
||||
} else if (numberMatch != null) {
|
||||
final matchText = numberMatch.group(0);
|
||||
final numText = numberMatch.group(1);
|
||||
if (matchText != null && numText != null) {
|
||||
@ -55,14 +62,6 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
|
||||
}
|
||||
}
|
||||
|
||||
if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) {
|
||||
return _toCheckboxList(editorState, textNode);
|
||||
} else if (_bulletedListSymbols.any(text.startsWith)) {
|
||||
return _toBulletedList(editorState, textNode);
|
||||
} else if (_countOfSign(text, selection) != 0) {
|
||||
return _toHeadingStyle(editorState, textNode, selection);
|
||||
}
|
||||
|
||||
return KeyEventResult.ignored;
|
||||
};
|
||||
|
||||
@ -196,7 +195,7 @@ KeyEventResult _toHeadingStyle(
|
||||
|
||||
int _countOfSign(String text, Selection selection) {
|
||||
for (var i = 6; i >= 0; i--) {
|
||||
if (text.substring(0, selection.end.offset).startsWith('#' * i)) {
|
||||
if (text.substring(0, selection.end.offset).contains('#' * i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
@ -174,5 +174,45 @@ void main() async {
|
||||
expect(textNode.subtype, BuiltInAttributeKey.bulletedList);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Presses whitespace key in edge cases', (tester) async {
|
||||
const text = '';
|
||||
final editor = tester.editor..insertTextNode(text);
|
||||
await editor.startTesting();
|
||||
|
||||
final textNode = editor.nodeAtPath([0]) as TextNode;
|
||||
await editor.updateSelection(
|
||||
Selection.single(path: [0], startOffset: 0),
|
||||
);
|
||||
|
||||
await editor.insertText(textNode, '*', 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.bulletedList);
|
||||
|
||||
await editor.insertText(textNode, '[]', 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.checkbox);
|
||||
expect(textNode.attributes.check, false);
|
||||
|
||||
await editor.insertText(textNode, '1.', 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.numberList);
|
||||
|
||||
await editor.insertText(textNode, '#', 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.heading);
|
||||
|
||||
await editor.insertText(textNode, '[x]', 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.checkbox);
|
||||
expect(textNode.attributes.check, true);
|
||||
|
||||
const insertedText = '[]AppFlowy';
|
||||
await editor.insertText(textNode, insertedText, 0);
|
||||
await editor.pressLogicKey(LogicalKeyboardKey.space);
|
||||
expect(textNode.subtype, BuiltInAttributeKey.checkbox);
|
||||
expect(textNode.attributes.check, true);
|
||||
expect(textNode.toRawString(), insertedText);
|
||||
});
|
||||
});
|
||||
}
|
||||
|