mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #786 from LucasXu0/feat/markdown_input
implement markdown input style, like, #, *, -, -[]
This commit is contained in:
@ -16,6 +16,7 @@ import 'package:flowy_editor/service/internal_key_event_handlers/delete_text_han
|
|||||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/slash_handler.dart';
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/whitespace_handler.dart';
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
import 'package:flowy_editor/service/scroll_service.dart';
|
import 'package:flowy_editor/service/scroll_service.dart';
|
||||||
@ -40,6 +41,7 @@ List<FlowyKeyEventHandler> defaultKeyEventHandler = [
|
|||||||
copyPasteKeysHandler,
|
copyPasteKeysHandler,
|
||||||
enterInEdgeOfTextNodeHandler,
|
enterInEdgeOfTextNodeHandler,
|
||||||
updateTextStyleByCommandXHandler,
|
updateTextStyleByCommandXHandler,
|
||||||
|
whiteSpaceHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
class FlowyEditor extends StatefulWidget {
|
class FlowyEditor extends StatefulWidget {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import 'package:flowy_editor/flowy_editor.dart';
|
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
|
||||||
// Handle delete text.
|
// Handle delete text.
|
||||||
FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
||||||
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
||||||
@ -28,9 +29,16 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
|||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
// 1. style
|
// 1. style
|
||||||
if (textNode.subtype != null) {
|
if (textNode.subtype != null) {
|
||||||
transactionBuilder.updateNode(textNode, {
|
transactionBuilder
|
||||||
'subtype': null,
|
..updateNode(textNode, {
|
||||||
});
|
'subtype': null,
|
||||||
|
})
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 2. non-style
|
// 2. non-style
|
||||||
// find previous text node.
|
// find previous text node.
|
||||||
|
@ -72,16 +72,22 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
|
|
||||||
final rect = selectable.getCursorRectInPosition(selection.start);
|
final rect = selectable.getCursorRectInPosition(selection.start);
|
||||||
final offset = selectable.localToGlobal(rect.topLeft);
|
final offset = selectable.localToGlobal(rect.topLeft);
|
||||||
if (!selection.isCollapsed) {
|
|
||||||
TransactionBuilder(editorState)
|
|
||||||
..deleteText(
|
|
||||||
textNode,
|
|
||||||
selection.start.offset,
|
|
||||||
selection.end.offset - selection.start.offset,
|
|
||||||
)
|
|
||||||
..commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..replaceText(textNode, selection.start.offset,
|
||||||
|
selection.end.offset - selection.start.offset, '/')
|
||||||
|
..commit();
|
||||||
|
|
||||||
|
_editorState = editorState;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
showPopupList(context, editorState, offset);
|
||||||
|
});
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
||||||
|
|
||||||
|
void showPopupList(
|
||||||
|
BuildContext context, EditorState editorState, Offset offset) {
|
||||||
_popupListOverlay?.remove();
|
_popupListOverlay?.remove();
|
||||||
_popupListOverlay = OverlayEntry(
|
_popupListOverlay = OverlayEntry(
|
||||||
builder: (context) => Positioned(
|
builder: (context) => Positioned(
|
||||||
@ -97,16 +103,12 @@ FlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
|
|||||||
Overlay.of(context)?.insert(_popupListOverlay!);
|
Overlay.of(context)?.insert(_popupListOverlay!);
|
||||||
|
|
||||||
editorState.service.selectionService.currentSelection
|
editorState.service.selectionService.currentSelection
|
||||||
.removeListener(clearPopupListOverlay);
|
.removeListener(clearPopupList);
|
||||||
editorState.service.selectionService.currentSelection
|
editorState.service.selectionService.currentSelection
|
||||||
.addListener(clearPopupListOverlay);
|
.addListener(clearPopupList);
|
||||||
// editorState.service.keyboardService?.disable();
|
}
|
||||||
_editorState = editorState;
|
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
void clearPopupList() {
|
||||||
};
|
|
||||||
|
|
||||||
void clearPopupListOverlay() {
|
|
||||||
_popupListOverlay?.remove();
|
_popupListOverlay?.remove();
|
||||||
_popupListOverlay = null;
|
_popupListOverlay = null;
|
||||||
|
|
||||||
@ -215,7 +217,7 @@ class _PopupListWidgetState extends State<PopupListWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
clearPopupListOverlay();
|
clearPopupList();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/document/node.dart';
|
||||||
|
import 'package:flowy_editor/document/position.dart';
|
||||||
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
|
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||||
|
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
|
||||||
|
const _bulletedListSymbols = ['*', '-'];
|
||||||
|
const _checkboxListSymbols = ['[x]', '-[x]'];
|
||||||
|
const _unCheckboxListSymbols = ['[]', '-[]'];
|
||||||
|
|
||||||
|
FlowyKeyEventHandler whiteSpaceHandler = (editorState, event) {
|
||||||
|
if (event.logicalKey != LogicalKeyboardKey.space) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process markdown input style.
|
||||||
|
///
|
||||||
|
/// like, #, *, -, 1., -[],
|
||||||
|
|
||||||
|
final selection = editorState.service.selectionService.currentSelection.value;
|
||||||
|
if (selection == null || !selection.isCollapsed) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final textNodes = editorState.service.selectionService.currentSelectedNodes
|
||||||
|
.whereType<TextNode>();
|
||||||
|
if (textNodes.length != 1) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
final text = textNode.toRawString();
|
||||||
|
if ((_checkboxListSymbols + _unCheckboxListSymbols).any(text.startsWith)) {
|
||||||
|
return _toCheckboxList(editorState, textNode);
|
||||||
|
} else if (_bulletedListSymbols.any(text.startsWith)) {
|
||||||
|
return _toBulletedList(editorState, textNode);
|
||||||
|
} else if (_countOfSign(text) != 0) {
|
||||||
|
return _toHeadingStyle(editorState, textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
|
||||||
|
if (textNode.subtype == StyleKey.bulletedList) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..deleteText(textNode, 0, 1)
|
||||||
|
..updateNode(textNode, {
|
||||||
|
StyleKey.subtype: StyleKey.bulletedList,
|
||||||
|
})
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
|
||||||
|
if (textNode.subtype == StyleKey.checkbox) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
final String symbol;
|
||||||
|
bool check = false;
|
||||||
|
final symbols = List<String>.from(_checkboxListSymbols)
|
||||||
|
..retainWhere(textNode.toRawString().startsWith);
|
||||||
|
if (symbols.isNotEmpty) {
|
||||||
|
symbol = symbols.first;
|
||||||
|
check = true;
|
||||||
|
} else {
|
||||||
|
symbol = (List<String>.from(_unCheckboxListSymbols)
|
||||||
|
..retainWhere(textNode.toRawString().startsWith))
|
||||||
|
.first;
|
||||||
|
check = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..deleteText(textNode, 0, symbol.length)
|
||||||
|
..updateNode(textNode, {
|
||||||
|
StyleKey.subtype: StyleKey.checkbox,
|
||||||
|
StyleKey.checkbox: check,
|
||||||
|
})
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyEventResult _toHeadingStyle(EditorState editorState, TextNode textNode) {
|
||||||
|
final x = _countOfSign(textNode.toRawString());
|
||||||
|
final hX = 'h$x';
|
||||||
|
if (textNode.attributes.heading == hX) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
TransactionBuilder(editorState)
|
||||||
|
..deleteText(textNode, 0, x)
|
||||||
|
..updateNode(textNode, {
|
||||||
|
StyleKey.subtype: StyleKey.heading,
|
||||||
|
StyleKey.heading: hX,
|
||||||
|
})
|
||||||
|
..afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: textNode.path,
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _countOfSign(String text) {
|
||||||
|
for (var i = 6; i >= 0; i--) {
|
||||||
|
if (text.startsWith('#' * i)) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
Reference in New Issue
Block a user