mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #754 from LucasXu0/feat/flowy_editor_input_service
feat: implement bold text in toolbar service
This commit is contained in:
commit
afd87a41d4
@ -144,7 +144,7 @@
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"subtype": "bullet-list"
|
||||
"subtype": "bulleted-list"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -155,7 +155,7 @@
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"subtype": "bullet-list"
|
||||
"subtype": "bulleted-list"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -170,7 +170,7 @@
|
||||
}
|
||||
],
|
||||
"attributes": {
|
||||
"subtype": "bullet-list"
|
||||
"subtype": "bulleted-list"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -116,13 +116,16 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
_editorState = EditorState(
|
||||
document: document,
|
||||
);
|
||||
return FlowyEditor(
|
||||
key: editorKey,
|
||||
editorState: _editorState,
|
||||
keyEventHandlers: const [],
|
||||
customBuilders: {
|
||||
'image': ImageNodeBuilder(),
|
||||
},
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(left: 20, right: 20),
|
||||
child: FlowyEditor(
|
||||
key: editorKey,
|
||||
editorState: _editorState,
|
||||
keyEventHandlers: const [],
|
||||
customBuilders: {
|
||||
'image': ImageNodeBuilder(),
|
||||
},
|
||||
),
|
||||
// shortcuts: [
|
||||
// // TODO: this won't work, just a example for now.
|
||||
// {
|
||||
|
@ -1,6 +1,23 @@
|
||||
import 'package:flowy_editor/flowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 1. define your custom type in example.json
|
||||
/// For example I need to define an image plugin, then I define type equals
|
||||
/// "image", and add "image_src" into "attributes".
|
||||
/// {
|
||||
/// "type": "image",
|
||||
/// "attributes", { "image_src": "https://s1.ax1x.com/2022/07/28/vCgz1x.png" }
|
||||
/// }
|
||||
/// 2. create a class extends [NodeWidgetBuilder]
|
||||
/// 3. override the function `Widget build(NodeWidgetContext<Node> context)`
|
||||
/// and return a widget to render. The returned widget should be
|
||||
/// a StatefulWidget and mixin with [Selectable].
|
||||
///
|
||||
/// 4. override the getter `nodeValidator`
|
||||
/// to verify the data structure in [Node].
|
||||
/// 5. register the plugin with `type` to `flowy_editor` in `main.dart`.
|
||||
/// 6. Congratulations!
|
||||
|
||||
class ImageNodeBuilder extends NodeWidgetBuilder<Node> {
|
||||
@override
|
||||
Widget build(NodeWidgetContext<Node> context) {
|
||||
|
@ -26,7 +26,7 @@ Attributes? composeAttributes(Attributes? a, Attributes? b) {
|
||||
a ??= {};
|
||||
b ??= {};
|
||||
final Attributes attributes = {};
|
||||
attributes.addAll(b);
|
||||
attributes.addAll(Map.from(b)..removeWhere((_, value) => value == null));
|
||||
|
||||
for (final entry in a.entries) {
|
||||
if (!b.containsKey(entry.key)) {
|
||||
|
@ -89,7 +89,11 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
|
||||
this.attributes['subtype'] != attributes['subtype'];
|
||||
|
||||
for (final attribute in attributes.entries) {
|
||||
this.attributes[attribute.key] = attribute.value;
|
||||
if (attribute.value == null) {
|
||||
this.attributes.remove(attribute.key);
|
||||
} else {
|
||||
this.attributes[attribute.key] = attribute.value;
|
||||
}
|
||||
}
|
||||
// Notify the new attributes
|
||||
// if attributes contains 'subtype', should notify parent to rebuild node
|
||||
@ -178,7 +182,7 @@ class TextNode extends Node {
|
||||
}) : _delta = delta;
|
||||
|
||||
TextNode.empty()
|
||||
: _delta = Delta([TextInsert('')]),
|
||||
: _delta = Delta([TextInsert(' ')]),
|
||||
super(
|
||||
type: 'text',
|
||||
children: LinkedList(),
|
||||
@ -201,6 +205,19 @@ class TextNode extends Node {
|
||||
return map;
|
||||
}
|
||||
|
||||
TextNode copyWith({
|
||||
String? type,
|
||||
LinkedList<Node>? children,
|
||||
Attributes? attributes,
|
||||
Delta? delta,
|
||||
}) =>
|
||||
TextNode(
|
||||
type: type ?? this.type,
|
||||
children: children ?? this.children,
|
||||
attributes: attributes ?? this.attributes,
|
||||
delta: delta ?? this.delta,
|
||||
);
|
||||
|
||||
// TODO: It's unneccesry to compute everytime.
|
||||
String toRawString() =>
|
||||
_delta.operations.whereType<TextInsert>().map((op) => op.content).join();
|
||||
|
@ -60,7 +60,12 @@ class EditorState {
|
||||
for (final op in transaction.operations) {
|
||||
_applyOperation(op);
|
||||
}
|
||||
updateCursorSelection(transaction.afterSelection);
|
||||
// updateCursorSelection(transaction.afterSelection);
|
||||
|
||||
// FIXME: don't use delay
|
||||
Future.delayed(const Duration(milliseconds: 16), () {
|
||||
updateCursorSelection(transaction.afterSelection);
|
||||
});
|
||||
|
||||
if (options.recordUndo) {
|
||||
final undoItem = undoManager.getUndoHistoryItem();
|
||||
|
@ -1,18 +1,18 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
|
||||
import 'package:flowy_editor/document/attributes.dart';
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/document/path.dart';
|
||||
import 'package:flowy_editor/document/position.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flowy_editor/document/attributes.dart';
|
||||
import 'package:flowy_editor/document/selection.dart';
|
||||
|
||||
import './operation.dart';
|
||||
import './transaction.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/operation/operation.dart';
|
||||
import 'package:flowy_editor/operation/transaction.dart';
|
||||
|
||||
/// A [TransactionBuilder] is used to build the transaction from the state.
|
||||
/// It will save make a snapshot of the cursor selection state automatically.
|
||||
/// The cursor can be resoted if the transaction is undo.
|
||||
/// The cursor can be resorted if the transaction is undo.
|
||||
|
||||
class TransactionBuilder {
|
||||
final List<Operation> operations = [];
|
||||
@ -70,7 +70,7 @@ class TransactionBuilder {
|
||||
}
|
||||
|
||||
textEdit(TextNode node, Delta Function() f) {
|
||||
beforeSelection = state.service.selectionService.currentSelection;
|
||||
beforeSelection = state.cursorSelection;
|
||||
final path = node.path;
|
||||
|
||||
final delta = f();
|
||||
|
@ -21,7 +21,7 @@ class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
|
||||
|
||||
@override
|
||||
NodeValidator<Node> get nodeValidator => ((node) {
|
||||
return node.attributes.containsKey(StyleKey.check);
|
||||
return node.attributes.containsKey(StyleKey.checkbox);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
|
||||
_renderParagraph.getOffsetForCaret(textPosition, Rect.zero);
|
||||
final cursorHeight = widget.cursorHeight ??
|
||||
_renderParagraph.getFullHeightForCaret(textPosition) ??
|
||||
5.0; // default height
|
||||
18.0; // default height
|
||||
return Rect.fromLTWH(
|
||||
cursorOffset.dx - (widget.cursorWidth / 2),
|
||||
cursorOffset.dy,
|
||||
|
@ -25,26 +25,49 @@ class StyleKey {
|
||||
static String font = 'font';
|
||||
static String href = 'href';
|
||||
|
||||
static String quote = 'quote';
|
||||
static String list = 'list';
|
||||
static String number = 'number';
|
||||
static String todo = 'todo';
|
||||
static String code = 'code';
|
||||
|
||||
static String subtype = 'subtype';
|
||||
static String check = 'checkbox';
|
||||
static String heading = 'heading';
|
||||
static String h1 = 'h1';
|
||||
static String h2 = 'h2';
|
||||
static String h3 = 'h3';
|
||||
static String h4 = 'h4';
|
||||
static String h5 = 'h5';
|
||||
static String h6 = 'h6';
|
||||
|
||||
static String bulletedList = 'bulleted-list';
|
||||
static String numberList = 'number-list';
|
||||
|
||||
static String quote = 'quote';
|
||||
static String checkbox = 'checkbox';
|
||||
static String code = 'code';
|
||||
static String number = 'number';
|
||||
|
||||
static List<String> partialStyleKeys = [
|
||||
StyleKey.bold,
|
||||
StyleKey.italic,
|
||||
StyleKey.underline,
|
||||
StyleKey.strikethrough,
|
||||
];
|
||||
|
||||
static List<String> globalStyleKeys = [
|
||||
StyleKey.heading,
|
||||
StyleKey.checkbox,
|
||||
StyleKey.bulletedList,
|
||||
StyleKey.numberList,
|
||||
StyleKey.quote,
|
||||
StyleKey.code,
|
||||
];
|
||||
}
|
||||
|
||||
double baseFontSize = 16.0;
|
||||
// TODO: customize.
|
||||
Map<String, double> headingToFontSize = {
|
||||
'h1': baseFontSize + 15,
|
||||
'h2': baseFontSize + 12,
|
||||
'h3': baseFontSize + 9,
|
||||
'h4': baseFontSize + 6,
|
||||
'h5': baseFontSize + 3,
|
||||
'h6': baseFontSize,
|
||||
StyleKey.h1: baseFontSize + 15,
|
||||
StyleKey.h2: baseFontSize + 12,
|
||||
StyleKey.h3: baseFontSize + 9,
|
||||
StyleKey.h4: baseFontSize + 6,
|
||||
StyleKey.h5: baseFontSize + 3,
|
||||
StyleKey.h6: baseFontSize,
|
||||
};
|
||||
|
||||
extension NodeAttributesExtensions on Attributes {
|
||||
@ -73,13 +96,6 @@ extension NodeAttributesExtensions on Attributes {
|
||||
return null;
|
||||
}
|
||||
|
||||
String? get list {
|
||||
if (containsKey(StyleKey.list) && this[StyleKey.list] is String) {
|
||||
return this[StyleKey.list];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? get number {
|
||||
if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
|
||||
return this[StyleKey.number];
|
||||
@ -87,13 +103,6 @@ extension NodeAttributesExtensions on Attributes {
|
||||
return null;
|
||||
}
|
||||
|
||||
bool get todo {
|
||||
if (containsKey(StyleKey.todo) && this[StyleKey.todo] is bool) {
|
||||
return this[StyleKey.todo];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get code {
|
||||
if (containsKey(StyleKey.code) && this[StyleKey.code] == true) {
|
||||
return this[StyleKey.code];
|
||||
@ -102,8 +111,8 @@ extension NodeAttributesExtensions on Attributes {
|
||||
}
|
||||
|
||||
bool get check {
|
||||
if (containsKey(StyleKey.check) && this[StyleKey.check] is bool) {
|
||||
return this[StyleKey.check];
|
||||
if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) {
|
||||
return this[StyleKey.checkbox];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -1,43 +1,39 @@
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/infra/flowy_svg.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
typedef ToolbarEventHandler = void Function(
|
||||
EditorState editorState, String eventName);
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/infra/flowy_svg.dart';
|
||||
import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart';
|
||||
|
||||
typedef ToolbarEventHandlers = List<Map<String, ToolbarEventHandler>>;
|
||||
ToolbarEventHandlers defaultToolbarEventHandlers = [
|
||||
{
|
||||
'bold': ((editorState, eventName) {}),
|
||||
'italic': ((editorState, eventName) {}),
|
||||
'strikethrough': ((editorState, eventName) {}),
|
||||
'underline': ((editorState, eventName) {}),
|
||||
'quote': ((editorState, eventName) {}),
|
||||
'number_list': ((editorState, eventName) {}),
|
||||
'bulleted_list': ((editorState, eventName) {}),
|
||||
}
|
||||
];
|
||||
typedef ToolbarEventHandler = void Function(EditorState editorState);
|
||||
|
||||
ToolbarEventHandlers defaultListToolbarEventHandlers = [
|
||||
{
|
||||
'h1': ((editorState, eventName) {}),
|
||||
},
|
||||
{
|
||||
'h2': ((editorState, eventName) {}),
|
||||
},
|
||||
{
|
||||
'h3': ((editorState, eventName) {}),
|
||||
},
|
||||
{
|
||||
'bulleted_list': ((editorState, eventName) {}),
|
||||
},
|
||||
{
|
||||
'quote': ((editorState, eventName) {}),
|
||||
}
|
||||
typedef ToolbarEventHandlers = Map<String, ToolbarEventHandler>;
|
||||
|
||||
ToolbarEventHandlers defaultToolbarEventHandlers = {
|
||||
'bold': (editorState) => formatBold(editorState),
|
||||
'italic': (editorState) => formatItalic(editorState),
|
||||
'strikethrough': (editorState) => formatStrikethrough(editorState),
|
||||
'underline': (editorState) => formatUnderline(editorState),
|
||||
'quote': (editorState) => formatQuote(editorState),
|
||||
'number_list': (editorState) {},
|
||||
'bulleted_list': (editorState) => formatBulletedList(editorState),
|
||||
'Text': (editorState) => formatText(editorState),
|
||||
'H1': (editorState) => formatHeading(editorState, StyleKey.h1),
|
||||
'H2': (editorState) => formatHeading(editorState, StyleKey.h2),
|
||||
'H3': (editorState) => formatHeading(editorState, StyleKey.h3),
|
||||
};
|
||||
|
||||
List<String> defaultListToolbarEventNames = [
|
||||
'Text',
|
||||
'H1',
|
||||
'H2',
|
||||
'H3',
|
||||
// 'B-List',
|
||||
// 'N-List',
|
||||
];
|
||||
|
||||
class ToolbarWidget extends StatefulWidget {
|
||||
ToolbarWidget({
|
||||
const ToolbarWidget({
|
||||
Key? key,
|
||||
required this.editorState,
|
||||
required this.layerLink,
|
||||
@ -137,7 +133,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
||||
preferBelow: false,
|
||||
message: name,
|
||||
child: GestureDetector(
|
||||
onTap: onTap ?? () => debugPrint('toolbar tap $name'),
|
||||
onTap: onTap ?? () => _onTap(name),
|
||||
child: SizedBox.fromSize(
|
||||
size: width != null
|
||||
? Size(width, toolbarHeight)
|
||||
@ -154,9 +150,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
||||
|
||||
void _onTapListToolbar(BuildContext context) {
|
||||
// TODO: implement more detailed UI.
|
||||
final items = defaultListToolbarEventHandlers
|
||||
.map((handler) => handler.keys.first)
|
||||
.toList(growable: false);
|
||||
final items = defaultListToolbarEventNames;
|
||||
final renderBox =
|
||||
_listToolbarKey.currentContext?.findRenderObject() as RenderBox;
|
||||
final offset = renderBox
|
||||
@ -198,7 +192,7 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
debugPrint('tap on $index');
|
||||
_onTap(items[index]);
|
||||
},
|
||||
);
|
||||
}),
|
||||
@ -210,6 +204,14 @@ class _ToolbarWidgetState extends State<ToolbarWidget> {
|
||||
Overlay.of(context)?.insert(_listToolbarOverlay!);
|
||||
}
|
||||
|
||||
void _onTap(String eventName) {
|
||||
if (defaultToolbarEventHandlers.containsKey(eventName)) {
|
||||
defaultToolbarEventHandlers[eventName]!(widget.editorState);
|
||||
return;
|
||||
}
|
||||
assert(false, 'Could not find the event handler for $eventName');
|
||||
}
|
||||
|
||||
void _onSelectionChange() {
|
||||
_listToolbarOverlay?.remove();
|
||||
_listToolbarOverlay = null;
|
||||
|
@ -1,9 +1,103 @@
|
||||
import 'package:flowy_editor/document/attributes.dart';
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/extensions/text_node_extensions.dart';
|
||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
|
||||
bool formatRichTextStyle(
|
||||
EditorState editorState, Map<String, dynamic> attributes) {
|
||||
void formatText(EditorState editorState) {
|
||||
formatTextNodes(editorState, {});
|
||||
}
|
||||
|
||||
void formatHeading(EditorState editorState, String heading) {
|
||||
formatTextNodes(editorState, {
|
||||
StyleKey.subtype: StyleKey.heading,
|
||||
StyleKey.heading: heading,
|
||||
});
|
||||
}
|
||||
|
||||
void formatQuote(EditorState editorState) {
|
||||
formatTextNodes(editorState, {
|
||||
StyleKey.subtype: StyleKey.quote,
|
||||
});
|
||||
}
|
||||
|
||||
void formatCheckbox(EditorState editorState) {
|
||||
formatTextNodes(editorState, {
|
||||
StyleKey.subtype: StyleKey.checkbox,
|
||||
StyleKey.checkbox: false,
|
||||
});
|
||||
}
|
||||
|
||||
void formatBulletedList(EditorState editorState) {
|
||||
formatTextNodes(editorState, {
|
||||
StyleKey.subtype: StyleKey.bulletedList,
|
||||
});
|
||||
}
|
||||
|
||||
bool formatTextNodes(EditorState editorState, Attributes attributes) {
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||
final textNodes = nodes.whereType<TextNode>().toList();
|
||||
|
||||
if (textNodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final builder = TransactionBuilder(editorState);
|
||||
|
||||
for (final textNode in textNodes) {
|
||||
builder.updateNode(
|
||||
textNode,
|
||||
Attributes.fromIterable(
|
||||
StyleKey.globalStyleKeys,
|
||||
value: (_) => null,
|
||||
)..addAll(attributes),
|
||||
);
|
||||
}
|
||||
|
||||
builder.commit();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool formatBold(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, StyleKey.bold);
|
||||
}
|
||||
|
||||
bool formatItalic(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, StyleKey.italic);
|
||||
}
|
||||
|
||||
bool formatUnderline(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, StyleKey.underline);
|
||||
}
|
||||
|
||||
bool formatStrikethrough(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, StyleKey.strikethrough);
|
||||
}
|
||||
|
||||
bool formatRichTextPartialStyle(EditorState editorState, String styleKey) {
|
||||
final selection = editorState.service.selectionService.currentSelection;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool value = !textNodes.allSatisfyInSelection(styleKey, selection);
|
||||
Attributes attributes = {
|
||||
styleKey: value,
|
||||
};
|
||||
if (styleKey == StyleKey.underline && value) {
|
||||
attributes[StyleKey.strikethrough] = null;
|
||||
} else if (styleKey == StyleKey.strikethrough && value) {
|
||||
attributes[StyleKey.underline] = null;
|
||||
}
|
||||
|
||||
return formatRichTextStyle(editorState, attributes);
|
||||
}
|
||||
|
||||
bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
final selection = editorState.service.selectionService.currentSelection;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||
final textNodes = nodes.whereType<TextNode>().toList();
|
||||
@ -17,29 +111,38 @@ bool formatRichTextStyle(
|
||||
// 1. All nodes are text nodes.
|
||||
// 2. The first node is not TextNode.
|
||||
// 3. The last node is not TextNode.
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final textNode = textNodes[i];
|
||||
if (i == 0 && textNode == nodes.first) {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
attributes,
|
||||
);
|
||||
} else if (i == textNodes.length - 1 && textNode == nodes.last) {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
0,
|
||||
selection.end.offset,
|
||||
attributes,
|
||||
);
|
||||
} else {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
0,
|
||||
textNode.toRawString().length,
|
||||
attributes,
|
||||
);
|
||||
if (nodes.length == textNodes.length && textNodes.length == 1) {
|
||||
builder.formatText(
|
||||
textNodes.first,
|
||||
selection.start.offset,
|
||||
selection.end.offset - selection.start.offset,
|
||||
attributes,
|
||||
);
|
||||
} else {
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final textNode = textNodes[i];
|
||||
if (i == 0 && textNode == nodes.first) {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
selection.start.offset,
|
||||
textNode.toRawString().length - selection.start.offset,
|
||||
attributes,
|
||||
);
|
||||
} else if (i == textNodes.length - 1 && textNode == nodes.last) {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
0,
|
||||
selection.end.offset,
|
||||
attributes,
|
||||
);
|
||||
} else {
|
||||
builder.formatText(
|
||||
textNode,
|
||||
0,
|
||||
textNode.toRawString().length,
|
||||
attributes,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ NodeWidgetBuilders defaultBuilders = {
|
||||
'text': RichTextNodeWidgetBuilder(),
|
||||
'text/checkbox': CheckboxNodeWidgetBuilder(),
|
||||
'text/heading': HeadingTextNodeWidgetBuilder(),
|
||||
'text/bullet-list': BulletedListTextNodeWidgetBuilder(),
|
||||
'text/bulleted-list': BulletedListTextNodeWidgetBuilder(),
|
||||
'text/number-list': NumberListTextNodeWidgetBuilder(),
|
||||
'text/quote': QuotedTextNodeWidgetBuilder(),
|
||||
};
|
||||
|
@ -1,12 +1,17 @@
|
||||
import 'dart:collection';
|
||||
|
||||
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/operation/transaction_builder.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
import 'package:flowy_editor/extensions/path_extensions.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flowy_editor/extensions/node_extensions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flowy_editor/extensions/path_extensions.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';
|
||||
|
||||
FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) {
|
||||
if (event.logicalKey != LogicalKeyboardKey.enter) {
|
||||
@ -23,20 +28,63 @@ FlowyKeyEventHandler enterInEdgeOfTextNodeHandler = (editorState, event) {
|
||||
}
|
||||
|
||||
final textNode = nodes.first as TextNode;
|
||||
|
||||
if (textNode.selectable!.end() == selection.end) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path.next,
|
||||
TextNode.empty(),
|
||||
)
|
||||
..commit();
|
||||
if (textNode.subtype != null && textNode.delta.length == 0) {
|
||||
TransactionBuilder(editorState)
|
||||
..deleteNode(textNode)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta([TextInsert('')]),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: textNode.path,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
} else {
|
||||
final needCopyAttributes = StyleKey.globalStyleKeys
|
||||
.where((key) => key != StyleKey.heading)
|
||||
.contains(textNode.subtype);
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path.next,
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta([TextInsert('')]),
|
||||
attributes: needCopyAttributes ? textNode.attributes : {},
|
||||
),
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: textNode.path.next,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
}
|
||||
|
||||
return KeyEventResult.handled;
|
||||
} else if (textNode.selectable!.start() == selection.start) {
|
||||
TransactionBuilder(editorState)
|
||||
..insertNode(
|
||||
textNode.path,
|
||||
TextNode.empty(),
|
||||
textNode.copyWith(
|
||||
children: LinkedList(),
|
||||
delta: Delta([TextInsert('')]),
|
||||
attributes: {},
|
||||
),
|
||||
)
|
||||
..afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: textNode.path.next,
|
||||
offset: 0,
|
||||
),
|
||||
)
|
||||
..commit();
|
||||
return KeyEventResult.handled;
|
||||
|
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/extensions/text_node_extensions.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
import 'package:flowy_editor/service/default_text_operations/format_rich_text_style.dart';
|
||||
import 'package:flowy_editor/service/keyboard_service.dart';
|
||||
|
||||
@ -23,9 +21,7 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
|
||||
// bold
|
||||
case 'B':
|
||||
case 'b':
|
||||
formatRichTextStyle(editorState, {
|
||||
StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection),
|
||||
});
|
||||
formatBold(editorState);
|
||||
return KeyEventResult.handled;
|
||||
default:
|
||||
break;
|
||||
|
@ -233,6 +233,9 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
clearSelection();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -455,7 +458,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
..forEach((overlay) => overlay.remove())
|
||||
..clear();
|
||||
// clear toolbar
|
||||
editorState.service.toolbarService.hide();
|
||||
editorState.service.toolbarService?.hide();
|
||||
}
|
||||
|
||||
void _updateSelection(Selection selection) {
|
||||
@ -526,7 +529,7 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
if (topmostRect != null && layerLink != null) {
|
||||
editorState.service.toolbarService
|
||||
.showInOffset(topmostRect.topLeft, layerLink);
|
||||
?.showInOffset(topmostRect.topLeft, layerLink);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,9 +23,11 @@ class FlowyService {
|
||||
|
||||
// toolbar service
|
||||
final toolbarServiceKey = GlobalKey(debugLabel: 'flowy_toolbar_service');
|
||||
ToolbarService get toolbarService {
|
||||
assert(toolbarServiceKey.currentState != null &&
|
||||
toolbarServiceKey.currentState is ToolbarService);
|
||||
return toolbarServiceKey.currentState! as ToolbarService;
|
||||
ToolbarService? get toolbarService {
|
||||
if (toolbarServiceKey.currentState != null &&
|
||||
toolbarServiceKey.currentState is ToolbarService) {
|
||||
return toolbarServiceKey.currentState! as ToolbarService;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ class _FlowyToolbarState extends State<FlowyToolbar> with ToolbarService {
|
||||
editorState: widget.editorState,
|
||||
layerLink: layerLink,
|
||||
offset: offset.translate(0, -37.0),
|
||||
handlers: const [],
|
||||
handlers: const {},
|
||||
),
|
||||
);
|
||||
Overlay.of(context)?.insert(_toolbarOverlay!);
|
||||
|
@ -8,6 +8,7 @@ import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/document/state_tree.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
group('transform path', () {
|
||||
test('transform path changed', () {
|
||||
expect(transformPath([0, 1], [0, 1]), [0, 2]);
|
||||
@ -87,7 +88,7 @@ void main() {
|
||||
"path": [0],
|
||||
"nodes": [item1.toJson()],
|
||||
}
|
||||
],
|
||||
]
|
||||
});
|
||||
});
|
||||
test("delete", () {
|
||||
|
Loading…
Reference in New Issue
Block a user