mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #743 from LucasXu0/feat/flowy_editor_input_service
Feat/flowy editor input service
This commit is contained in:
commit
63d8e18051
@ -3,17 +3,6 @@
|
|||||||
"type": "editor",
|
"type": "editor",
|
||||||
"attributes": {},
|
"attributes": {},
|
||||||
"children": [
|
"children": [
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"delta": [
|
|
||||||
{
|
|
||||||
"insert": "Hello world"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attributes": {
|
|
||||||
"subtype": "quote"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "image",
|
"type": "image",
|
||||||
"attributes": {
|
"attributes": {
|
||||||
@ -173,7 +162,11 @@
|
|||||||
"type": "text",
|
"type": "text",
|
||||||
"delta": [
|
"delta": [
|
||||||
{
|
{
|
||||||
"insert": "Hello world"
|
"insert": "Hello "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"insert": "world",
|
||||||
|
"attributes": { "bold": true }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"attributes": {
|
"attributes": {
|
||||||
|
@ -55,6 +55,7 @@ class MyHomePage extends StatefulWidget {
|
|||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
late EditorState _editorState;
|
late EditorState _editorState;
|
||||||
|
final editorKey = GlobalKey();
|
||||||
int page = 0;
|
int page = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -116,6 +117,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
document: document,
|
document: document,
|
||||||
);
|
);
|
||||||
return FlowyEditor(
|
return FlowyEditor(
|
||||||
|
key: editorKey,
|
||||||
editorState: _editorState,
|
editorState: _editorState,
|
||||||
keyEventHandlers: const [],
|
keyEventHandlers: const [],
|
||||||
customBuilders: {
|
customBuilders: {
|
||||||
|
@ -275,6 +275,11 @@ class Delta {
|
|||||||
|
|
||||||
Delta([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[];
|
Delta([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[];
|
||||||
|
|
||||||
|
Delta addAll(List<TextOperation> textOps) {
|
||||||
|
textOps.forEach(add);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
Delta add(TextOperation textOp) {
|
Delta add(TextOperation textOp) {
|
||||||
if (textOp.isEmpty) {
|
if (textOp.isEmpty) {
|
||||||
return this;
|
return this;
|
||||||
|
@ -48,6 +48,10 @@ class TransactionBuilder {
|
|||||||
add(DeleteOperation(path: node.path, removedValue: node));
|
add(DeleteOperation(path: node.path, removedValue: node));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteNodes(List<Node> nodes) {
|
||||||
|
nodes.forEach(deleteNode);
|
||||||
|
}
|
||||||
|
|
||||||
textEdit(TextNode node, Delta Function() f) {
|
textEdit(TextNode node, Delta Function() f) {
|
||||||
beforeSelection = state.cursorSelection;
|
beforeSelection = state.cursorSelection;
|
||||||
final path = node.path;
|
final path = node.path;
|
||||||
@ -59,8 +63,28 @@ class TransactionBuilder {
|
|||||||
add(TextEditOperation(path: path, delta: delta, inverted: inverted));
|
add(TextEditOperation(path: path, delta: delta, inverted: inverted));
|
||||||
}
|
}
|
||||||
|
|
||||||
insertText(TextNode node, int index, String content) {
|
mergeText(TextNode firstNode, TextNode secondNode,
|
||||||
textEdit(node, () => Delta().retain(index).insert(content));
|
{int? firstOffset, int secondOffset = 0}) {
|
||||||
|
final firstLength = firstNode.delta.length;
|
||||||
|
final secondLength = secondNode.delta.length;
|
||||||
|
textEdit(
|
||||||
|
firstNode,
|
||||||
|
() => Delta()
|
||||||
|
..retain(firstOffset ?? firstLength)
|
||||||
|
..delete(firstLength - (firstOffset ?? firstLength))
|
||||||
|
..addAll(secondNode.delta.slice(secondOffset, secondLength).operations),
|
||||||
|
);
|
||||||
|
afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: firstNode.path,
|
||||||
|
offset: firstOffset ?? firstLength,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertText(TextNode node, int index, String content,
|
||||||
|
[Attributes? attributes]) {
|
||||||
|
textEdit(node, () => Delta().retain(index).insert(content, attributes));
|
||||||
afterSelection = Selection.collapsed(
|
afterSelection = Selection.collapsed(
|
||||||
Position(path: node.path, offset: index + content.length));
|
Position(path: node.path, offset: index + content.length));
|
||||||
}
|
}
|
||||||
@ -75,6 +99,19 @@ class TransactionBuilder {
|
|||||||
Selection.collapsed(Position(path: node.path, offset: index));
|
Selection.collapsed(Position(path: node.path, offset: index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replaceText(TextNode node, int index, int length, String content) {
|
||||||
|
textEdit(
|
||||||
|
node,
|
||||||
|
() => Delta().retain(index).delete(length).insert(content),
|
||||||
|
);
|
||||||
|
afterSelection = Selection.collapsed(
|
||||||
|
Position(
|
||||||
|
path: node.path,
|
||||||
|
offset: index + content.length,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
add(Operation op) {
|
add(Operation op) {
|
||||||
final Operation? last = operations.isEmpty ? null : operations.last;
|
final Operation? last = operations.isEmpty ? null : operations.last;
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
|
@ -64,10 +64,14 @@ class CursorWidgetState extends State<CursorWidget> {
|
|||||||
link: widget.layerLink,
|
link: widget.layerLink,
|
||||||
offset: widget.rect.topCenter,
|
offset: widget.rect.topCenter,
|
||||||
showWhenUnlinked: true,
|
showWhenUnlinked: true,
|
||||||
|
// Ignore the gestures in cursor
|
||||||
|
// to solve the problem that cursor area cannot be selected.
|
||||||
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: showCursor ? widget.color : Colors.transparent,
|
color: showCursor ? widget.color : Colors.transparent,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,14 @@ class _SelectionWidgetState extends State<SelectionWidget> {
|
|||||||
link: widget.layerLink,
|
link: widget.layerLink,
|
||||||
offset: widget.rect.topLeft,
|
offset: widget.rect.topLeft,
|
||||||
showWhenUnlinked: true,
|
showWhenUnlinked: true,
|
||||||
|
// Ignore the gestures in selection overlays
|
||||||
|
// to solve the problem that selection areas cannot overlap.
|
||||||
|
child: IgnorePointer(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: widget.color,
|
color: widget.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,24 +1,25 @@
|
|||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/delele_text_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/render/editor/editor_entry.dart';
|
import 'package:flowy_editor/render/editor/editor_entry.dart';
|
||||||
|
import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
|
import 'package:flowy_editor/render/rich_text/checkbox_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
|
import 'package:flowy_editor/render/rich_text/flowy_rich_text.dart';
|
||||||
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
|
||||||
import 'package:flowy_editor/service/input_service.dart';
|
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/enter_in_edge_of_text_node_handler.dart';
|
|
||||||
import 'package:flowy_editor/service/render_plugin_service.dart';
|
|
||||||
import 'package:flowy_editor/service/shortcut_service.dart';
|
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_handler.dart';
|
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/delete_single_text_node_handler.dart';
|
|
||||||
import 'package:flowy_editor/service/internal_key_event_handlers/shortcut_handler.dart';
|
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
|
||||||
import 'package:flowy_editor/service/selection_service.dart';
|
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
|
||||||
import 'package:flowy_editor/render/rich_text/bulleted_list_text.dart';
|
|
||||||
import 'package:flowy_editor/render/rich_text/heading_text.dart';
|
import 'package:flowy_editor/render/rich_text/heading_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/number_list_text.dart';
|
import 'package:flowy_editor/render/rich_text/number_list_text.dart';
|
||||||
import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
import 'package:flowy_editor/render/rich_text/quoted_text.dart';
|
||||||
|
import 'package:flowy_editor/render/selection/floating_shortcut_widget.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flowy_editor/service/input_service.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/arrow_keys_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/internal_key_event_handlers/delete_nodes_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/shortcut_handler.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
import 'package:flowy_editor/service/render_plugin_service.dart';
|
||||||
|
import 'package:flowy_editor/service/selection_service.dart';
|
||||||
|
import 'package:flowy_editor/service/shortcut_service.dart';
|
||||||
|
|
||||||
NodeWidgetBuilders defaultBuilders = {
|
NodeWidgetBuilders defaultBuilders = {
|
||||||
'editor': EditorEntryWidgetBuilder(),
|
'editor': EditorEntryWidgetBuilder(),
|
||||||
@ -30,6 +31,15 @@ NodeWidgetBuilders defaultBuilders = {
|
|||||||
'text/quote': QuotedTextNodeWidgetBuilder(),
|
'text/quote': QuotedTextNodeWidgetBuilder(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
List<FlowyKeyEventHandler> defaultKeyEventHandler = [
|
||||||
|
deleteTextHandler,
|
||||||
|
slashShortcutHandler,
|
||||||
|
flowyDeleteNodesHandler,
|
||||||
|
arrowKeysHandler,
|
||||||
|
enterInEdgeOfTextNodeHandler,
|
||||||
|
updateTextStyleByCommandXHandler,
|
||||||
|
];
|
||||||
|
|
||||||
class FlowyEditor extends StatefulWidget {
|
class FlowyEditor extends StatefulWidget {
|
||||||
const FlowyEditor({
|
const FlowyEditor({
|
||||||
Key? key,
|
Key? key,
|
||||||
@ -61,13 +71,16 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
editorState.service.renderPluginService = FlowyRenderPlugin(
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
editorState: editorState,
|
}
|
||||||
builders: {
|
|
||||||
...defaultBuilders,
|
@override
|
||||||
...widget.customBuilders,
|
void didUpdateWidget(covariant FlowyEditor oldWidget) {
|
||||||
},
|
super.didUpdateWidget(oldWidget);
|
||||||
);
|
|
||||||
|
if (editorState.service != oldWidget.editorState.service) {
|
||||||
|
editorState.service.renderPluginService = _createRenderPlugin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -81,11 +94,7 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
child: FlowyKeyboard(
|
child: FlowyKeyboard(
|
||||||
key: editorState.service.keyboardServiceKey,
|
key: editorState.service.keyboardServiceKey,
|
||||||
handlers: [
|
handlers: [
|
||||||
slashShortcutHandler,
|
...defaultKeyEventHandler,
|
||||||
flowyDeleteNodesHandler,
|
|
||||||
deleteSingleTextNodeHandler,
|
|
||||||
arrowKeysHandler,
|
|
||||||
enterInEdgeOfTextNodeHandler,
|
|
||||||
...widget.keyEventHandlers,
|
...widget.keyEventHandlers,
|
||||||
],
|
],
|
||||||
editorState: editorState,
|
editorState: editorState,
|
||||||
@ -106,4 +115,12 @@ class _FlowyEditorState extends State<FlowyEditor> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlowyRenderPlugin _createRenderPlugin() => FlowyRenderPlugin(
|
||||||
|
editorState: editorState,
|
||||||
|
builders: {
|
||||||
|
...defaultBuilders,
|
||||||
|
...widget.customBuilders,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
|
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/position.dart';
|
||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flowy_editor/editor_state.dart';
|
import 'package:flowy_editor/editor_state.dart';
|
||||||
import 'package:flowy_editor/document/node.dart';
|
|
||||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
mixin FlowyInputService {
|
mixin FlowyInputService {
|
||||||
void attach(TextEditingValue textEditingValue);
|
void attach(TextEditingValue textEditingValue);
|
||||||
@ -93,8 +94,10 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
// TODO: implement the detail
|
// TODO: implement the detail
|
||||||
for (final delta in deltas) {
|
for (final delta in deltas) {
|
||||||
if (delta is TextEditingDeltaInsertion) {
|
if (delta is TextEditingDeltaInsertion) {
|
||||||
|
_applyInsert(delta);
|
||||||
} else if (delta is TextEditingDeltaDeletion) {
|
} else if (delta is TextEditingDeltaDeletion) {
|
||||||
} else if (delta is TextEditingDeltaReplacement) {
|
} else if (delta is TextEditingDeltaReplacement) {
|
||||||
|
_applyReplacement(delta);
|
||||||
} else if (delta is TextEditingDeltaNonTextUpdate) {
|
} else if (delta is TextEditingDeltaNonTextUpdate) {
|
||||||
// We don't need to care the [TextEditingDeltaNonTextUpdate].
|
// We don't need to care the [TextEditingDeltaNonTextUpdate].
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
@ -102,6 +105,46 @@ class _FlowyInputState extends State<FlowyInput>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _applyInsert(TextEditingDeltaInsertion delta) {
|
||||||
|
final selectionService = _editorState.service.selectionService;
|
||||||
|
final currentSelection = selectionService.currentSelection;
|
||||||
|
if (currentSelection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSelection.isSingle) {
|
||||||
|
final textNode =
|
||||||
|
selectionService.currentSelectedNodes.value.first as TextNode;
|
||||||
|
TransactionBuilder(_editorState)
|
||||||
|
..insertText(
|
||||||
|
textNode,
|
||||||
|
delta.insertionOffset,
|
||||||
|
delta.textInserted,
|
||||||
|
)
|
||||||
|
..commit();
|
||||||
|
} else {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyReplacement(TextEditingDeltaReplacement delta) {
|
||||||
|
final selectionService = _editorState.service.selectionService;
|
||||||
|
final currentSelection = selectionService.currentSelection;
|
||||||
|
if (currentSelection == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentSelection.isSingle) {
|
||||||
|
final textNode =
|
||||||
|
selectionService.currentSelectedNodes.value.first as TextNode;
|
||||||
|
final length = delta.replacedRange.end - delta.replacedRange.start;
|
||||||
|
TransactionBuilder(_editorState)
|
||||||
|
..replaceText(
|
||||||
|
textNode, delta.replacedRange.start, length, delta.replacementText)
|
||||||
|
..commit();
|
||||||
|
} else {
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void close() {
|
void close() {
|
||||||
_textInputConnection?.close();
|
_textInputConnection?.close();
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
import 'package:flowy_editor/flowy_editor.dart';
|
||||||
|
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
// Handle delete text.
|
||||||
|
FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
|
||||||
|
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.service.selectionService.currentSelection;
|
||||||
|
if (selection == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||||
|
// make sure all nodes is [TextNode].
|
||||||
|
final textNodes = nodes.whereType<TextNode>().toList();
|
||||||
|
if (textNodes.length != nodes.length) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionBuilder transactionBuilder = TransactionBuilder(editorState);
|
||||||
|
if (textNodes.length == 1) {
|
||||||
|
final textNode = textNodes.first;
|
||||||
|
final index = selection.start.offset - 1;
|
||||||
|
if (index < 0) {
|
||||||
|
// 1. style
|
||||||
|
if (textNode.subtype != null) {
|
||||||
|
transactionBuilder.updateNode(textNode, {
|
||||||
|
'subtype': null,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 2. non-style
|
||||||
|
// find previous text node.
|
||||||
|
while (textNode.previous != null) {
|
||||||
|
if (textNode.previous is TextNode) {
|
||||||
|
final previous = textNode.previous as TextNode;
|
||||||
|
transactionBuilder
|
||||||
|
..deleteNode(textNode)
|
||||||
|
..mergeText(previous, textNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selection.isCollapsed) {
|
||||||
|
transactionBuilder.deleteText(
|
||||||
|
textNode,
|
||||||
|
selection.start.offset - 1,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
transactionBuilder.deleteText(
|
||||||
|
textNode,
|
||||||
|
selection.start.offset,
|
||||||
|
selection.end.offset - selection.start.offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final first = textNodes.first;
|
||||||
|
final last = textNodes.last;
|
||||||
|
var content = textNodes.last.toRawString();
|
||||||
|
content = content.substring(selection.end.offset, content.length);
|
||||||
|
// Merge the fist and the last text node content,
|
||||||
|
// and delete the all nodes expect for the first.
|
||||||
|
transactionBuilder
|
||||||
|
..deleteNodes(textNodes.sublist(1))
|
||||||
|
..mergeText(
|
||||||
|
first,
|
||||||
|
last,
|
||||||
|
firstOffset: selection.start.offset,
|
||||||
|
secondOffset: selection.end.offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionBuilder.commit();
|
||||||
|
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
};
|
@ -1,69 +0,0 @@
|
|||||||
import 'package:flowy_editor/document/node.dart';
|
|
||||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
|
||||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
|
||||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
|
||||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
// TODO: need to be refactored, just a example code.
|
|
||||||
FlowyKeyEventHandler deleteSingleTextNodeHandler = (editorState, event) {
|
|
||||||
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
}
|
|
||||||
|
|
||||||
// final selectionNodes = editorState.selectedNodes;
|
|
||||||
// if (selectionNodes.length == 1 && selectionNodes.first is TextNode) {
|
|
||||||
// final node = selectionNodes.first.unwrapOrNull<TextNode>();
|
|
||||||
// final selectable = node?.key?.currentState?.unwrapOrNull<Selectable>();
|
|
||||||
// if (selectable != null) {
|
|
||||||
// final textSelection = selectable.getCurrentTextSelection();
|
|
||||||
// if (textSelection != null) {
|
|
||||||
// if (textSelection.isCollapsed) {
|
|
||||||
// /// Three cases:
|
|
||||||
// /// Delete the zero character,
|
|
||||||
// /// 1. if there is still text node in front of it, then merge them.
|
|
||||||
// /// 2. if not, just ignore
|
|
||||||
// /// Delete the non-zero character,
|
|
||||||
// /// 3. delete the single character.
|
|
||||||
// if (textSelection.baseOffset == 0) {
|
|
||||||
// if (node?.previous != null && node?.previous is TextNode) {
|
|
||||||
// final previous = node!.previous! as TextNode;
|
|
||||||
// final newTextSelection = TextSelection.collapsed(
|
|
||||||
// offset: previous.toRawString().length);
|
|
||||||
// final selectionService = editorState.service.selectionService;
|
|
||||||
// final previousSelectable =
|
|
||||||
// previous.key?.currentState?.unwrapOrNull<Selectable>();
|
|
||||||
// final newOfset = previousSelectable
|
|
||||||
// ?.getOffsetByTextSelection(newTextSelection);
|
|
||||||
// if (newOfset != null) {
|
|
||||||
// // selectionService.updateCursor(newOfset);
|
|
||||||
// }
|
|
||||||
// // merge
|
|
||||||
// TransactionBuilder(editorState)
|
|
||||||
// ..deleteNode(node)
|
|
||||||
// ..insertText(
|
|
||||||
// previous, previous.toRawString().length, node.toRawString())
|
|
||||||
// ..commit();
|
|
||||||
// return KeyEventResult.handled;
|
|
||||||
// } else {
|
|
||||||
// return KeyEventResult.ignored;
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// TransactionBuilder(editorState)
|
|
||||||
// ..deleteText(node!, textSelection.baseOffset - 1, 1)
|
|
||||||
// ..commit();
|
|
||||||
// final newTextSelection =
|
|
||||||
// TextSelection.collapsed(offset: textSelection.baseOffset - 1);
|
|
||||||
// final selectionService = editorState.service.selectionService;
|
|
||||||
// final newOfset =
|
|
||||||
// selectable.getOffsetByTextSelection(newTextSelection);
|
|
||||||
// // selectionService.updateCursor(newOfset);
|
|
||||||
// return KeyEventResult.handled;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
return KeyEventResult.ignored;
|
|
||||||
};
|
|
@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flowy_editor/document/node.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/service/keyboard_service.dart';
|
||||||
|
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
|
||||||
|
if (!event.isMetaPressed || event.character == null) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
final selection = editorState.service.selectionService.currentSelection;
|
||||||
|
final nodes = editorState.service.selectionService.currentSelectedNodes.value
|
||||||
|
.whereType<TextNode>()
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (selection == null || nodes.isEmpty) {
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.character!) {
|
||||||
|
// bold
|
||||||
|
case 'B':
|
||||||
|
case 'b':
|
||||||
|
_makeBold(editorState, nodes, selection);
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return KeyEventResult.ignored;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: implement unBold.
|
||||||
|
void _makeBold(
|
||||||
|
EditorState editorState, List<TextNode> nodes, Selection selection) {
|
||||||
|
final builder = TransactionBuilder(editorState);
|
||||||
|
if (nodes.length == 1) {
|
||||||
|
builder.formatText(
|
||||||
|
nodes.first,
|
||||||
|
selection.start.offset,
|
||||||
|
selection.end.offset - selection.start.offset,
|
||||||
|
{
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < nodes.length; i++) {
|
||||||
|
final node = nodes[i];
|
||||||
|
if (i == 0) {
|
||||||
|
builder.formatText(
|
||||||
|
node,
|
||||||
|
selection.start.offset,
|
||||||
|
node.toRawString().length - selection.start.offset,
|
||||||
|
{
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (i == nodes.length - 1) {
|
||||||
|
builder.formatText(
|
||||||
|
node,
|
||||||
|
0,
|
||||||
|
selection.end.offset,
|
||||||
|
{
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
builder.formatText(
|
||||||
|
node,
|
||||||
|
0,
|
||||||
|
node.toRawString().length,
|
||||||
|
{
|
||||||
|
'bold': true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.commit();
|
||||||
|
}
|
@ -75,10 +75,7 @@ class FlowyRenderPlugin extends FlowyRenderPluginService {
|
|||||||
if (builder != null && builder.nodeValidator(node)) {
|
if (builder != null && builder.nodeValidator(node)) {
|
||||||
final key = GlobalKey(debugLabel: name);
|
final key = GlobalKey(debugLabel: name);
|
||||||
node.key = key;
|
node.key = key;
|
||||||
return _wrap(
|
return _autoUpdateNodeWidget(builder, context);
|
||||||
builder.build(context),
|
|
||||||
context,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
assert(false, 'Could not query the builder with this $name');
|
assert(false, 'Could not query the builder with this $name');
|
||||||
// TODO: return a placeholder widget with tips.
|
// TODO: return a placeholder widget with tips.
|
||||||
@ -87,14 +84,14 @@ class FlowyRenderPlugin extends FlowyRenderPluginService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void register(String name, NodeWidgetBuilder<Node> builder) {
|
void register(String name, NodeWidgetBuilder builder) {
|
||||||
debugPrint('[Plugins] registering $name...');
|
debugPrint('[Plugins] registering $name...');
|
||||||
_validatePlugin(name);
|
_validatePlugin(name);
|
||||||
_builders[name] = builder;
|
_builders[name] = builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void registerAll(Map<String, NodeWidgetBuilder<Node>> builders) {
|
void registerAll(Map<String, NodeWidgetBuilder> builders) {
|
||||||
builders.forEach(register);
|
builders.forEach(register);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,18 +101,35 @@ class FlowyRenderPlugin extends FlowyRenderPluginService {
|
|||||||
_builders.remove(name);
|
_builders.remove(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _wrap(Widget widget, NodeWidgetContext context) {
|
Widget _autoUpdateNodeWidget(
|
||||||
|
NodeWidgetBuilder builder, NodeWidgetContext context) {
|
||||||
|
Widget notifier;
|
||||||
|
if (context.node is TextNode) {
|
||||||
|
notifier = ChangeNotifierProvider.value(
|
||||||
|
value: context.node as TextNode,
|
||||||
|
builder: (_, child) {
|
||||||
|
return Consumer<TextNode>(
|
||||||
|
builder: ((_, value, child) {
|
||||||
|
debugPrint('Text Node is rebuilding...');
|
||||||
|
return builder.build(context);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notifier = ChangeNotifierProvider.value(
|
||||||
|
value: context.node,
|
||||||
|
builder: (_, child) {
|
||||||
|
return Consumer<Node>(
|
||||||
|
builder: ((_, value, child) {
|
||||||
|
debugPrint('Node is rebuilding...');
|
||||||
|
return builder.build(context);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
return CompositedTransformTarget(
|
return CompositedTransformTarget(
|
||||||
link: context.node.layerLink,
|
link: context.node.layerLink,
|
||||||
child: ChangeNotifierProvider<Node>.value(
|
child: notifier,
|
||||||
value: context.node,
|
|
||||||
builder: (context, child) => Consumer(
|
|
||||||
builder: ((context, value, child) {
|
|
||||||
debugPrint('Node is rebuilding...');
|
|
||||||
return widget;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import 'package:flowy_editor/document/position.dart';
|
|||||||
import 'package:flowy_editor/document/selection.dart';
|
import 'package:flowy_editor/document/selection.dart';
|
||||||
import 'package:flowy_editor/render/selection/selectable.dart';
|
import 'package:flowy_editor/render/selection/selectable.dart';
|
||||||
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
import 'package:flowy_editor/render/selection/cursor_widget.dart';
|
||||||
import 'package:flowy_editor/render/selection/flowy_selection_widget.dart';
|
import 'package:flowy_editor/render/selection/selection_widget.dart';
|
||||||
import 'package:flowy_editor/extensions/object_extensions.dart';
|
import 'package:flowy_editor/extensions/object_extensions.dart';
|
||||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
|
Loading…
x
Reference in New Issue
Block a user