mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: implement edit text style by command + x
This commit is contained in:
parent
06cab949f2
commit
159fe63575
@ -0,0 +1,88 @@
|
||||
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/selection.dart';
|
||||
import 'package:flowy_editor/document/text_delta.dart';
|
||||
import 'package:flowy_editor/render/rich_text/rich_text_style.dart';
|
||||
|
||||
extension TextNodeExtension on TextNode {
|
||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.bold, selection);
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.italic, selection);
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.underline, selection);
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.strikethrough, selection);
|
||||
|
||||
bool allSatisfyInSelection(String styleKey, Selection selection) {
|
||||
final ops = delta.operations.whereType<TextInsert>();
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= selection.end.offset) {
|
||||
break;
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < selection.end.offset &&
|
||||
start + length > selection.start.offset) {
|
||||
if (op.attributes == null ||
|
||||
!op.attributes!.containsKey(styleKey) ||
|
||||
op.attributes![styleKey] == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
start += length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
extension TextNodesExtension on List<TextNode> {
|
||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.bold, selection);
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.italic, selection);
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.underline, selection);
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(StyleKey.strikethrough, selection);
|
||||
|
||||
bool allSatisfyInSelection(String styleKey, Selection selection) {
|
||||
if (isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (length == 1) {
|
||||
return first.allSatisfyInSelection(styleKey, selection);
|
||||
} else {
|
||||
for (var i = 0; i < length; i++) {
|
||||
final node = this[i];
|
||||
final Selection newSelection;
|
||||
if (i == 0 && pathEquals(node.path, selection.start.path)) {
|
||||
newSelection = selection.copyWith(
|
||||
end: Position(path: node.path, offset: node.toRawString().length),
|
||||
);
|
||||
} else if (i == length - 1 &&
|
||||
pathEquals(node.path, selection.end.path)) {
|
||||
newSelection = selection.copyWith(
|
||||
start: Position(path: node.path, offset: 0),
|
||||
);
|
||||
} else {
|
||||
newSelection = Selection(
|
||||
start: Position(path: node.path, offset: 0),
|
||||
end: Position(path: node.path, offset: node.toRawString().length),
|
||||
);
|
||||
}
|
||||
if (!node.allSatisfyInSelection(styleKey, newSelection)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import 'package:flowy_editor/document/node.dart';
|
||||
import 'package:flowy_editor/editor_state.dart';
|
||||
import 'package:flowy_editor/operation/transaction_builder.dart';
|
||||
|
||||
bool formatRichTextStyle(
|
||||
EditorState editorState, Map<String, dynamic> attributes) {
|
||||
final selection = editorState.service.selectionService.currentSelection;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||
final textNodes = nodes.whereType<TextNode>().toList();
|
||||
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final builder = TransactionBuilder(editorState);
|
||||
|
||||
// 1. All nodes are text nodes.
|
||||
// 2. The first node is not TextNode.
|
||||
// 3. The last node is not TextNode.
|
||||
if (textNodes.length == nodes.length) {
|
||||
if (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 node = textNodes[i];
|
||||
if (i == 0) {
|
||||
builder.formatText(
|
||||
node,
|
||||
selection.start.offset,
|
||||
node.toRawString().length - selection.start.offset,
|
||||
attributes,
|
||||
);
|
||||
} else if (i == textNodes.length - 1) {
|
||||
builder.formatText(
|
||||
node,
|
||||
0,
|
||||
selection.end.offset,
|
||||
attributes,
|
||||
);
|
||||
} else {
|
||||
builder.formatText(
|
||||
node,
|
||||
0,
|
||||
node.toRawString().length,
|
||||
attributes,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final node = textNodes[i];
|
||||
if (i == 0 && node == nodes.first) {
|
||||
builder.formatText(
|
||||
node,
|
||||
selection.start.offset,
|
||||
node.toRawString().length - selection.start.offset,
|
||||
attributes,
|
||||
);
|
||||
} else if (i == textNodes.length - 1 && node == nodes.last) {
|
||||
builder.formatText(
|
||||
node,
|
||||
0,
|
||||
selection.end.offset,
|
||||
attributes,
|
||||
);
|
||||
} else {
|
||||
builder.formatText(
|
||||
node,
|
||||
0,
|
||||
node.toRawString().length,
|
||||
attributes,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builder.commit();
|
||||
|
||||
return true;
|
||||
}
|
@ -1,22 +1,21 @@
|
||||
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';
|
||||
|
||||
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';
|
||||
|
||||
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();
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes.value;
|
||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||
|
||||
if (selection == null || nodes.isEmpty) {
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
@ -24,7 +23,9 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
|
||||
// bold
|
||||
case 'B':
|
||||
case 'b':
|
||||
_makeBold(editorState, nodes, selection);
|
||||
formatRichTextStyle(editorState, {
|
||||
StyleKey.bold: !textNodes.allSatisfyBoldInSelection(selection),
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
default:
|
||||
break;
|
||||
@ -32,52 +33,3 @@ FlowyKeyEventHandler updateTextStyleByCommandXHandler = (editorState, event) {
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -422,14 +422,19 @@ class _FlowySelectionState extends State<FlowySelection>
|
||||
|
||||
// compute the selection in range.
|
||||
if (first != null && last != null) {
|
||||
bool isDownward = panStartOffset!.dy <= panEndOffset!.dy;
|
||||
bool isDownward;
|
||||
if (first == last) {
|
||||
isDownward = panStartOffset!.dx < panEndOffset!.dx;
|
||||
} else {
|
||||
isDownward = panStartOffset!.dy < panEndOffset!.dy;
|
||||
}
|
||||
final start =
|
||||
first.getSelectionInRange(panStartOffset!, panEndOffset!).start;
|
||||
final end = last.getSelectionInRange(panStartOffset!, panEndOffset!).end;
|
||||
final selection = Selection(
|
||||
start: isDownward ? start : end, end: isDownward ? end : start);
|
||||
debugPrint('[_onPanUpdate] $selection');
|
||||
editorState.updateCursorSelection(selection);
|
||||
debugPrint('[_onPanUpdate] isDownward = $isDownward, $selection');
|
||||
editorState.service.selectionService.updateSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user