feat: remove the reference of the deprecated values (#304)
* feat: remove the reference of the deprecated values * chore: add migration guide
This commit is contained in:
parent
f8da435ad5
commit
5f136df356
40 changed files with 281 additions and 3459 deletions
|
|
@ -115,6 +115,9 @@ Below are some examples of shortcut event customizations:
|
|||
* [BIUS](https://github.com/AppFlowy-IO/appflowy-editor/tree/main/lib/src/editor/editor_component/service/shortcuts/character_shortcut_events/format_single_character) demonstrates how to make text bold/italic/underline/strikethrough through shortcut keys
|
||||
* Need more examples? Check out [shortcuts](https://github.com/AppFlowy-IO/appflowy-editor/tree/main/lib/src/editor/editor_component/service/shortcuts)
|
||||
|
||||
## Migration Guide
|
||||
Please refer to the [migration documentation]((https://github.com/AppFlowy-IO/appflowy-editor/blob/main/documentation/migration.md)).
|
||||
|
||||
## Glossary
|
||||
Please refer to the API documentation.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,5 @@ linter:
|
|||
- use_decorated_box
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
deprecated_member_use_from_same_package: ignore
|
||||
exclude:
|
||||
- lib/src/l10n/**
|
||||
9
documentation/migration.md
Normal file
9
documentation/migration.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Migration Guide
|
||||
|
||||
## From 1.1.0 to 1.2.0
|
||||
|
||||
- `AppFlowyEditor.custom` and `AppFlowyEditor.standard` have been removed. Use `AppFlowyEditor.` instead.
|
||||
- For now, we provide the default values to the `blockComponentBuilders`, `characterShortcutEvents`, and `commandShortcutEvents` if you do not customize them.
|
||||
- `DefaultSelectable` has been renamed to `DefaultSelectableMixin`
|
||||
- `FlowyRichText` has been renamed to `AppFlowyRichText`.
|
||||
- Added new parameter called `context`(BuildContext) into `TextSpanDecoratorForAttribute`.
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import 'package:appflowy_editor/src/commands/command_extension.dart';
|
||||
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
|
||||
extension TextCommands on EditorState {
|
||||
Future<void> updateNodeAttributes(
|
||||
Attributes attributes, {
|
||||
Path? path,
|
||||
Node? node,
|
||||
}) {
|
||||
return futureCommand(() {
|
||||
final n = getNode(path: path, node: node);
|
||||
apply(
|
||||
transaction..updateNode(n, attributes),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/editor_state.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
extension CommandExtension on EditorState {
|
||||
Future<void> futureCommand(void Function() fn) async {
|
||||
final completer = Completer<void>();
|
||||
fn();
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
completer.complete();
|
||||
});
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Node getNode({
|
||||
Path? path,
|
||||
Node? node,
|
||||
}) {
|
||||
if (node != null) {
|
||||
return node;
|
||||
} else if (path != null) {
|
||||
return document.nodeAtPath(path)!;
|
||||
}
|
||||
throw Exception('path and node cannot be null at the same time');
|
||||
}
|
||||
|
||||
TextNode getTextNode({
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) {
|
||||
if (textNode != null) {
|
||||
return textNode;
|
||||
} else if (path != null) {
|
||||
return document.nodeAtPath(path)! as TextNode;
|
||||
}
|
||||
throw Exception('path and node cannot be null at the same time');
|
||||
}
|
||||
|
||||
Selection getSelection(
|
||||
Selection? selection,
|
||||
) {
|
||||
final currentSelection = service.selectionService.currentSelection.value;
|
||||
if (selection != null) {
|
||||
return selection;
|
||||
} else if (currentSelection != null) {
|
||||
return currentSelection;
|
||||
}
|
||||
throw Exception('path and textNode cannot be null at the same time');
|
||||
}
|
||||
|
||||
List<String> getTextInSelection(
|
||||
List<TextNode> textNodes,
|
||||
Selection selection,
|
||||
) {
|
||||
List<String> res = [];
|
||||
if (selection.isSingle) {
|
||||
final plainText = textNodes.first.toPlainText();
|
||||
res.add(plainText.substring(selection.startIndex, selection.endIndex));
|
||||
} else if (!selection.isCollapsed) {
|
||||
for (var i = 0; i < textNodes.length; i++) {
|
||||
final plainText = textNodes[i].toPlainText();
|
||||
if (i == 0) {
|
||||
res.add(
|
||||
plainText.substring(
|
||||
selection.startIndex,
|
||||
plainText.length,
|
||||
),
|
||||
);
|
||||
} else if (i == textNodes.length - 1) {
|
||||
res.add(plainText.substring(0, selection.endIndex));
|
||||
} else {
|
||||
res.add(plainText);
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/commands/command_extension.dart';
|
||||
|
||||
extension TextCommands on EditorState {
|
||||
/// Insert text at the given index of the given [TextNode] or the [Path].
|
||||
///
|
||||
/// [Path] and [TextNode] are mutually exclusive.
|
||||
/// One of these two parameters must have a value.
|
||||
Future<void> insertText(
|
||||
int index,
|
||||
String text, {
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) async {
|
||||
final n = getTextNode(path: path, textNode: textNode);
|
||||
return apply(
|
||||
transaction..insertText(n, index, text),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertTextAtCurrentSelection(String text) async {
|
||||
final selection = getSelection(null);
|
||||
assert(selection.isCollapsed);
|
||||
final textNode = getTextNode(path: selection.start.path);
|
||||
return insertText(
|
||||
selection.startIndex,
|
||||
text,
|
||||
textNode: textNode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> formatText(
|
||||
Selection? selection,
|
||||
Attributes attributes, {
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) async {
|
||||
final n = getTextNode(path: path, textNode: textNode);
|
||||
final s = getSelection(selection);
|
||||
return apply(
|
||||
transaction..formatText(n, s.startIndex, s.length, attributes),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> formatTextWithBuiltInAttribute(
|
||||
String key,
|
||||
Attributes attributes, {
|
||||
Selection? selection,
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) async {
|
||||
final n = getTextNode(path: path, textNode: textNode);
|
||||
if (BuiltInAttributeKey.globalStyleKeys.contains(key)) {
|
||||
final attr = n.attributes
|
||||
..removeWhere(
|
||||
(key, _) => BuiltInAttributeKey.globalStyleKeys.contains(key),
|
||||
)
|
||||
..addAll(attributes)
|
||||
..addAll({
|
||||
BuiltInAttributeKey.subtype: key,
|
||||
});
|
||||
return apply(
|
||||
transaction..updateNode(n, attr),
|
||||
);
|
||||
} else if (BuiltInAttributeKey.partialStyleKeys.contains(key)) {
|
||||
final s = getSelection(selection);
|
||||
return apply(
|
||||
transaction..formatText(n, s.startIndex, s.length, attributes),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> formatTextToCheckbox(
|
||||
bool check, {
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) async {
|
||||
return formatTextWithBuiltInAttribute(
|
||||
BuiltInAttributeKey.checkbox,
|
||||
{BuiltInAttributeKey.checkbox: check},
|
||||
path: path,
|
||||
textNode: textNode,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> formatLinkInText(
|
||||
String? link, {
|
||||
Path? path,
|
||||
TextNode? textNode,
|
||||
}) async {
|
||||
return formatTextWithBuiltInAttribute(
|
||||
BuiltInAttributeKey.href,
|
||||
{BuiltInAttributeKey.href: link},
|
||||
path: path,
|
||||
textNode: textNode,
|
||||
);
|
||||
}
|
||||
|
||||
// Future<void> insertNewLine({
|
||||
// Path? path,
|
||||
// }) async {
|
||||
// final p = path ?? getSelection(null).start.path.next;
|
||||
// final transaction = this.transaction;
|
||||
// transaction.insertNode(p, TextNode.empty());
|
||||
// transaction.afterSelection = Selection.single(
|
||||
// path: p,
|
||||
// startOffset: 0,
|
||||
// );
|
||||
// return apply(transaction);
|
||||
// }
|
||||
|
||||
Future<void> insertNewLineAtCurrentSelection() async {
|
||||
final selection = getSelection(null);
|
||||
assert(selection.isCollapsed);
|
||||
final textNode = getTextNode(path: selection.start.path);
|
||||
final transaction = this.transaction;
|
||||
transaction.splitText(
|
||||
textNode,
|
||||
selection.startIndex,
|
||||
);
|
||||
return apply(transaction);
|
||||
}
|
||||
}
|
||||
|
|
@ -119,16 +119,17 @@ class Document {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Updates the [TextNode] at the given [Path]
|
||||
/// Updates the [Node] with [Delta] at the given [Path]
|
||||
bool updateText(Path path, Delta delta) {
|
||||
if (path.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final target = nodeAtPath(path);
|
||||
if (target == null || target is! TextNode) {
|
||||
final targetDelta = target?.delta;
|
||||
if (target == null || targetDelta == null) {
|
||||
return false;
|
||||
}
|
||||
target.delta = target.delta.compose(delta);
|
||||
target.updateAttributes({'delta': (targetDelta.compose(delta)).toJson()});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -125,12 +125,6 @@ class Transaction {
|
|||
insertNode(path, node, deepCopy: false);
|
||||
}
|
||||
|
||||
/// Update the [TextNode]s with the given [Delta].
|
||||
void updateText(TextNode textNode, Delta delta) {
|
||||
final inverted = delta.invert(textNode.delta);
|
||||
add(UpdateTextOperation(textNode.path, delta, inverted));
|
||||
}
|
||||
|
||||
/// Returns the JSON representation of the transaction.
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
|
@ -551,126 +545,6 @@ extension TextTransaction on Transaction {
|
|||
markNeedsComposing = true;
|
||||
_composeMap.putIfAbsent(node, () => []).add(delta);
|
||||
}
|
||||
|
||||
// the below code is deprecated.
|
||||
void splitText(TextNode textNode, int offset) {
|
||||
final delta = textNode.delta;
|
||||
final second = delta.slice(offset, delta.length);
|
||||
final path = textNode.path.next;
|
||||
deleteText(textNode, offset, delta.length);
|
||||
insertNode(
|
||||
path,
|
||||
TextNode(
|
||||
attributes: textNode.attributes,
|
||||
delta: second,
|
||||
),
|
||||
);
|
||||
afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
offset: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Inserts the text content at a specified index.
|
||||
///
|
||||
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
|
||||
/// By default, the formatting attributes before the insert position will be reused.
|
||||
// void insertText(
|
||||
// TextNode textNode,
|
||||
// int index,
|
||||
// String text, {
|
||||
// Attributes? attributes,
|
||||
// }) {
|
||||
// var newAttributes = attributes;
|
||||
// if (index != 0 && attributes == null) {
|
||||
// newAttributes =
|
||||
// textNode.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
// if (newAttributes != null) {
|
||||
// newAttributes = {...newAttributes}; // make a copy
|
||||
// }
|
||||
// }
|
||||
// updateText(
|
||||
// textNode,
|
||||
// Delta()
|
||||
// ..retain(index)
|
||||
// ..insert(text, attributes: newAttributes),
|
||||
// );
|
||||
// afterSelection = Selection.collapsed(
|
||||
// Position(path: textNode.path, offset: index + text.length),
|
||||
// );
|
||||
// }
|
||||
|
||||
/// Assigns a formatting attributes to a range of text.
|
||||
// void formatText(
|
||||
// TextNode textNode,
|
||||
// int index,
|
||||
// int length,
|
||||
// Attributes attributes,
|
||||
// ) {
|
||||
// afterSelection = beforeSelection;
|
||||
// updateText(
|
||||
// textNode,
|
||||
// Delta()
|
||||
// ..retain(index)
|
||||
// ..retain(length, attributes: attributes),
|
||||
// );
|
||||
// }
|
||||
|
||||
// /// Deletes the text of specified length starting at index.
|
||||
// void deleteText(
|
||||
// TextNode textNode,
|
||||
// int index,
|
||||
// int length,
|
||||
// ) {
|
||||
// updateText(
|
||||
// textNode,
|
||||
// Delta()
|
||||
// ..retain(index)
|
||||
// ..delete(length),
|
||||
// );
|
||||
// afterSelection = Selection.collapsed(
|
||||
// Position(path: textNode.path, offset: index),
|
||||
// );
|
||||
// }
|
||||
|
||||
/// Replaces the text of specified length starting at index.
|
||||
///
|
||||
/// Optionally, you may specify formatting attributes that are applied to the inserted string.
|
||||
/// By default, the formatting attributes before the insert position will be reused.
|
||||
// void replaceText(
|
||||
// TextNode textNode,
|
||||
// int index,
|
||||
// int length,
|
||||
// String text, {
|
||||
// Attributes? attributes,
|
||||
// }) {
|
||||
// var newAttributes = attributes;
|
||||
// if (index != 0 && attributes == null) {
|
||||
// newAttributes =
|
||||
// textNode.delta.slice(max(index - 1, 0), index).first.attributes;
|
||||
// if (newAttributes == null) {
|
||||
// final slicedDelta = textNode.delta.slice(index, index + length);
|
||||
// if (slicedDelta.isNotEmpty) {
|
||||
// newAttributes = slicedDelta.first.attributes;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// updateText(
|
||||
// textNode,
|
||||
// Delta()
|
||||
// ..retain(index)
|
||||
// ..delete(length)
|
||||
// ..insert(text, attributes: {...newAttributes ?? {}}),
|
||||
// );
|
||||
// afterSelection = Selection.collapsed(
|
||||
// Position(
|
||||
// path: textNode.path,
|
||||
// offset: index + text.length,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
}
|
||||
|
||||
extension on Delta {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,12 @@ Node paragraphNode({
|
|||
Attributes? attributes,
|
||||
Iterable<Node> children = const [],
|
||||
}) {
|
||||
attributes ??= {
|
||||
ParagraphBlockKeys.delta: (delta ?? (Delta()..insert(text ?? ''))).toJson(),
|
||||
};
|
||||
return Node(
|
||||
type: ParagraphBlockKeys.type,
|
||||
attributes: {
|
||||
...attributes,
|
||||
ParagraphBlockKeys.delta:
|
||||
(delta ?? (Delta()..insert(text ?? ''))).toJson(),
|
||||
if (attributes != null) ...attributes,
|
||||
if (textDirection != null)
|
||||
ParagraphBlockKeys.textDirection: textDirection,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
final selection = currentSelection.value;
|
||||
if (selection == null ||
|
||||
(selection.isCollapsed == true &&
|
||||
currentSelectedNodes.first is TextNode)) {
|
||||
currentSelectedNodes.first.delta != null)) {
|
||||
_onDoubleTapDown(details);
|
||||
}
|
||||
|
||||
|
|
@ -529,7 +529,7 @@ class _DesktopSelectionServiceWidgetState
|
|||
_clearContextMenu();
|
||||
|
||||
// For now, only support the text node.
|
||||
if (!currentSelectedNodes.every((element) => element is TextNode)) {
|
||||
if (!currentSelectedNodes.every((element) => element.delta != null)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,16 +31,15 @@ CommandShortcutEventHandler _copyCommandHandler = (editorState) {
|
|||
|
||||
// rich text.
|
||||
// TODO: support rich text. the below code is not working.
|
||||
final nodes = editorState.getNodesInSelection(selection);
|
||||
final html = NodesToHTMLConverter(
|
||||
nodes: nodes,
|
||||
startOffset: selection.startIndex,
|
||||
endOffset: selection.endIndex,
|
||||
).toHTMLString();
|
||||
// final nodes = editorState.getNodesInSelection(selection);
|
||||
// final html = NodesToHTMLConverter(
|
||||
// nodes: nodes,
|
||||
// startOffset: selection.startIndex,
|
||||
// endOffset: selection.endIndex,
|
||||
// ).toHTMLString();
|
||||
|
||||
AppFlowyClipboard.setData(
|
||||
text: text,
|
||||
html: html.isEmpty ? null : html,
|
||||
);
|
||||
|
||||
return KeyEventResult.handled;
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension EditorStateExtensions on EditorState {
|
||||
List<TextNode> get selectedTextNodes =>
|
||||
service.selectionService.currentSelectedNodes
|
||||
.whereType<TextNode>()
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
export 'attributes_extension.dart';
|
||||
export 'color_extension.dart';
|
||||
export 'editor_state_extensions.dart';
|
||||
export 'node_extensions.dart';
|
||||
export 'object_extensions.dart';
|
||||
export 'position_extension.dart';
|
||||
export 'text_node_extensions.dart';
|
||||
export 'text_style_extension.dart';
|
||||
export 'url_launcher_extension.dart';
|
||||
|
|
|
|||
|
|
@ -1,221 +0,0 @@
|
|||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
import 'package:appflowy_editor/src/core/document/path.dart';
|
||||
import 'package:appflowy_editor/src/core/location/position.dart';
|
||||
import 'package:appflowy_editor/src/core/location/selection.dart';
|
||||
import 'package:appflowy_editor/src/core/document/text_delta.dart';
|
||||
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
|
||||
|
||||
extension TextNodeExtension on TextNode {
|
||||
T? getAttributeInSelection<T>(Selection selection, String styleKey) {
|
||||
final ops = delta.whereType<TextInsert>();
|
||||
final startOffset =
|
||||
selection.isBackward ? selection.start.offset : selection.end.offset;
|
||||
final endOffset =
|
||||
selection.isBackward ? selection.end.offset : selection.start.offset;
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= endOffset) {
|
||||
break;
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < endOffset && start + length > startOffset) {
|
||||
final attributes = op.attributes;
|
||||
if (attributes != null && attributes[styleKey] is T?) {
|
||||
return attributes[styleKey];
|
||||
}
|
||||
}
|
||||
start += length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool allSatisfyLinkInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.href, (value) {
|
||||
return value != null;
|
||||
});
|
||||
|
||||
bool allSatisfyFontColorInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.textColor, (value) {
|
||||
return value != null;
|
||||
});
|
||||
|
||||
bool allSatisfyBackgroundColorInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.highlightColor,
|
||||
(value) {
|
||||
return value != null;
|
||||
});
|
||||
|
||||
bool allSatisfyBoldInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.italic, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.underline, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough,
|
||||
(value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyCodeInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(selection, BuiltInAttributeKey.code, (value) {
|
||||
return value == true;
|
||||
});
|
||||
|
||||
bool allSatisfyInSelection(
|
||||
Selection selection,
|
||||
String styleKey,
|
||||
bool Function(dynamic value) test,
|
||||
) {
|
||||
if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) {
|
||||
if (attributes.containsKey(styleKey)) {
|
||||
return test(attributes[styleKey]);
|
||||
}
|
||||
} else if (BuiltInAttributeKey.partialStyleKeys.contains(styleKey)) {
|
||||
final ops = delta.whereType<TextInsert>();
|
||||
final startOffset =
|
||||
selection.isBackward ? selection.start.offset : selection.end.offset;
|
||||
final endOffset =
|
||||
selection.isBackward ? selection.end.offset : selection.start.offset;
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= endOffset) {
|
||||
break;
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < endOffset && start + length > startOffset) {
|
||||
if (op.attributes == null ||
|
||||
!op.attributes!.containsKey(styleKey) ||
|
||||
!test(op.attributes![styleKey])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
start += length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allNotSatisfyInSelection(
|
||||
String styleKey,
|
||||
dynamic value,
|
||||
Selection selection,
|
||||
) {
|
||||
final ops = delta.whereType<TextInsert>();
|
||||
final startOffset =
|
||||
selection.isBackward ? selection.start.offset : selection.end.offset;
|
||||
final endOffset =
|
||||
selection.isBackward ? selection.end.offset : selection.start.offset;
|
||||
var start = 0;
|
||||
for (final op in ops) {
|
||||
if (start >= endOffset) {
|
||||
break;
|
||||
}
|
||||
final length = op.length;
|
||||
if (start < endOffset && start + length > startOffset) {
|
||||
if (op.attributes != null &&
|
||||
op.attributes!.containsKey(styleKey) &&
|
||||
op.attributes![styleKey] == value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
start += length;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get isNotBulletOrCheckbox => ![
|
||||
BuiltInAttributeKey.bulletedList,
|
||||
BuiltInAttributeKey.checkbox
|
||||
].contains('');
|
||||
}
|
||||
|
||||
extension TextNodesExtension on List<TextNode> {
|
||||
bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
|
||||
selection,
|
||||
BuiltInAttributeKey.bold,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyItalicInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
BuiltInAttributeKey.italic,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyUnderlineInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
BuiltInAttributeKey.underline,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyStrikethroughInSelection(Selection selection) =>
|
||||
allSatisfyInSelection(
|
||||
selection,
|
||||
BuiltInAttributeKey.strikethrough,
|
||||
(value) => value == true,
|
||||
);
|
||||
|
||||
bool allSatisfyInSelection(
|
||||
Selection selection,
|
||||
String styleKey,
|
||||
bool Function(dynamic value) test,
|
||||
) {
|
||||
if (isEmpty) {
|
||||
return false;
|
||||
}
|
||||
if (length == 1) {
|
||||
return first.allSatisfyInSelection(selection, styleKey, (value) {
|
||||
return test(value);
|
||||
});
|
||||
} else {
|
||||
for (var i = 0; i < length; i++) {
|
||||
final node = this[i];
|
||||
final Selection newSelection;
|
||||
if (i == 0 && node.path.equals(selection.start.path)) {
|
||||
if (selection.isBackward) {
|
||||
newSelection = selection.copyWith(
|
||||
end: Position(path: node.path, offset: node.toPlainText().length),
|
||||
);
|
||||
} else {
|
||||
newSelection = selection.copyWith(
|
||||
end: Position(path: node.path, offset: 0),
|
||||
);
|
||||
}
|
||||
} else if (i == length - 1 && node.path.equals(selection.end.path)) {
|
||||
if (selection.isBackward) {
|
||||
newSelection = selection.copyWith(
|
||||
start: Position(path: node.path, offset: 0),
|
||||
);
|
||||
} else {
|
||||
newSelection = selection.copyWith(
|
||||
start:
|
||||
Position(path: node.path, offset: node.toPlainText().length),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
newSelection = Selection(
|
||||
start: Position(path: node.path, offset: 0),
|
||||
end: Position(path: node.path, offset: node.toPlainText().length),
|
||||
);
|
||||
}
|
||||
if (!node.allSatisfyInSelection(newSelection, styleKey, test)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
import 'dart:collection';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:html/dom.dart' as html;
|
||||
|
||||
|
|
@ -400,7 +398,7 @@ class NodesToHTMLConverter {
|
|||
/// </ul>
|
||||
///
|
||||
/// This container is used to save the list elements temporarily.
|
||||
html.Element? _stashListContainer;
|
||||
// html.Element? _stashListContainer;
|
||||
|
||||
NodesToHTMLConverter({
|
||||
required this.nodes,
|
||||
|
|
@ -411,72 +409,63 @@ class NodesToHTMLConverter {
|
|||
return;
|
||||
} else if (nodes.length == 1) {
|
||||
final first = nodes.first;
|
||||
if (first is TextNode) {
|
||||
if (first.delta != null) {
|
||||
nodes[0] = first.copyWith(
|
||||
delta: first.delta.slice(startOffset ?? 0, endOffset),
|
||||
attributes: {
|
||||
...first.attributes,
|
||||
'delta': (first.delta!.slice(startOffset ?? 0, endOffset)).toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final first = nodes.first;
|
||||
final last = nodes.last;
|
||||
if (first is TextNode) {
|
||||
nodes[0] = first.copyWith(delta: first.delta.slice(startOffset ?? 0));
|
||||
if (first.delta != null) {
|
||||
nodes[0] = first.copyWith(
|
||||
attributes: {
|
||||
...first.attributes,
|
||||
'delta': (first.delta!.slice(startOffset ?? 0)).toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
if (last is TextNode) {
|
||||
nodes[nodes.length - 1] =
|
||||
last.copyWith(delta: last.delta.slice(0, endOffset));
|
||||
if (last.delta != null) {
|
||||
nodes[nodes.length - 1] = first.copyWith(
|
||||
attributes: {
|
||||
...first.attributes,
|
||||
'delta': (last.delta!.slice(0, endOffset)).toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<html.Node> toHTMLNodes() {
|
||||
for (final node in nodes) {
|
||||
if (node.type == 'text') {
|
||||
final textNode = node as TextNode;
|
||||
if (node == nodes.first) {
|
||||
_addTextNode(textNode);
|
||||
} else if (node == nodes.last) {
|
||||
_addTextNode(textNode, end: endOffset);
|
||||
} else {
|
||||
_addTextNode(textNode);
|
||||
}
|
||||
} else if (node.type == 'image') {
|
||||
final textNode = node;
|
||||
final anchor = html.Element.tag(HTMLTag.image);
|
||||
anchor.attributes['src'] = textNode.attributes['image_src'];
|
||||
// for (final node in nodes) {
|
||||
// if (node.type == 'text') {
|
||||
// final textNode = node as TextNode;
|
||||
// if (node == nodes.first) {
|
||||
// _addTextNode(textNode);
|
||||
// } else if (node == nodes.last) {
|
||||
// _addTextNode(textNode, end: endOffset);
|
||||
// } else {
|
||||
// _addTextNode(textNode);
|
||||
// }
|
||||
// } else if (node.type == 'image') {
|
||||
// final textNode = node;
|
||||
// final anchor = html.Element.tag(HTMLTag.image);
|
||||
// anchor.attributes['src'] = textNode.attributes['image_src'];
|
||||
|
||||
_result.add(anchor);
|
||||
}
|
||||
// TODO: handle image and other blocks
|
||||
}
|
||||
if (_stashListContainer != null) {
|
||||
_result.add(_stashListContainer!);
|
||||
_stashListContainer = null;
|
||||
}
|
||||
// _result.add(anchor);
|
||||
// }
|
||||
// // TODO: handle image and other blocks
|
||||
// }
|
||||
// if (_stashListContainer != null) {
|
||||
// _result.add(_stashListContainer!);
|
||||
// _stashListContainer = null;
|
||||
// }
|
||||
return _result;
|
||||
}
|
||||
|
||||
void _addTextNode(TextNode textNode, {int? end}) {
|
||||
_addElement(textNode, _textNodeToHtml(textNode, end: end));
|
||||
}
|
||||
|
||||
void _addElement(TextNode textNode, html.Element element) {
|
||||
if (element.localName == HTMLTag.list) {
|
||||
final isNumbered =
|
||||
textNode.attributes['subtype'] == BuiltInAttributeKey.numberList;
|
||||
_stashListContainer ??= html.Element.tag(
|
||||
isNumbered ? HTMLTag.orderedList : HTMLTag.unorderedList,
|
||||
);
|
||||
_stashListContainer?.append(element);
|
||||
} else {
|
||||
if (_stashListContainer != null) {
|
||||
_result.add(_stashListContainer!);
|
||||
_stashListContainer = null;
|
||||
}
|
||||
_result.add(element);
|
||||
}
|
||||
}
|
||||
|
||||
String toHTMLString() {
|
||||
final elements = toHTMLNodes();
|
||||
final copyString = elements.fold<String>(
|
||||
|
|
@ -486,68 +475,56 @@ class NodesToHTMLConverter {
|
|||
return copyString;
|
||||
}
|
||||
|
||||
html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
|
||||
String? subType = textNode.attributes['subtype'];
|
||||
String? heading = textNode.attributes['heading'];
|
||||
return _deltaToHtml(
|
||||
textNode.delta,
|
||||
subType: subType,
|
||||
heading: heading,
|
||||
end: end,
|
||||
checked: textNode.attributes['checkbox'] == true,
|
||||
);
|
||||
}
|
||||
// String _textDecorationsFromAttributes(Attributes attributes) {
|
||||
// var textDecoration = <String>[];
|
||||
// if (attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||
// textDecoration.add('line-through');
|
||||
// }
|
||||
// if (attributes[BuiltInAttributeKey.underline] == true) {
|
||||
// textDecoration.add('underline');
|
||||
// }
|
||||
|
||||
String _textDecorationsFromAttributes(Attributes attributes) {
|
||||
var textDecoration = <String>[];
|
||||
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||
textDecoration.add('line-through');
|
||||
}
|
||||
if (attributes[BuiltInAttributeKey.underline] == true) {
|
||||
textDecoration.add('underline');
|
||||
}
|
||||
// return textDecoration.join(' ');
|
||||
// }
|
||||
|
||||
return textDecoration.join(' ');
|
||||
}
|
||||
// String _attributesToCssStyle(Map<String, dynamic> attributes) {
|
||||
// final cssMap = <String, String>{};
|
||||
// if (attributes[BuiltInAttributeKey.highlightColor] != null) {
|
||||
// final color = Color(
|
||||
// int.parse(attributes[BuiltInAttributeKey.highlightColor]),
|
||||
// );
|
||||
// cssMap['background-color'] = color.toRgbaString();
|
||||
// }
|
||||
// if (attributes[BuiltInAttributeKey.textColor] != null) {
|
||||
// final color = Color(
|
||||
// int.parse(attributes[BuiltInAttributeKey.textColor]),
|
||||
// );
|
||||
// cssMap['color'] = color.toRgbaString();
|
||||
// }
|
||||
// if (attributes[BuiltInAttributeKey.bold] == true) {
|
||||
// cssMap['font-weight'] = 'bold';
|
||||
// }
|
||||
|
||||
String _attributesToCssStyle(Map<String, dynamic> attributes) {
|
||||
final cssMap = <String, String>{};
|
||||
if (attributes[BuiltInAttributeKey.highlightColor] != null) {
|
||||
final color = Color(
|
||||
int.parse(attributes[BuiltInAttributeKey.highlightColor]),
|
||||
);
|
||||
cssMap['background-color'] = color.toRgbaString();
|
||||
}
|
||||
if (attributes[BuiltInAttributeKey.textColor] != null) {
|
||||
final color = Color(
|
||||
int.parse(attributes[BuiltInAttributeKey.textColor]),
|
||||
);
|
||||
cssMap['color'] = color.toRgbaString();
|
||||
}
|
||||
if (attributes[BuiltInAttributeKey.bold] == true) {
|
||||
cssMap['font-weight'] = 'bold';
|
||||
}
|
||||
// final textDecoration = _textDecorationsFromAttributes(attributes);
|
||||
// if (textDecoration.isNotEmpty) {
|
||||
// cssMap['text-decoration'] = textDecoration;
|
||||
// }
|
||||
|
||||
final textDecoration = _textDecorationsFromAttributes(attributes);
|
||||
if (textDecoration.isNotEmpty) {
|
||||
cssMap['text-decoration'] = textDecoration;
|
||||
}
|
||||
// if (attributes[BuiltInAttributeKey.italic] == true) {
|
||||
// cssMap['font-style'] = 'italic';
|
||||
// }
|
||||
// return _cssMapToCssStyle(cssMap);
|
||||
// }
|
||||
|
||||
if (attributes[BuiltInAttributeKey.italic] == true) {
|
||||
cssMap['font-style'] = 'italic';
|
||||
}
|
||||
return _cssMapToCssStyle(cssMap);
|
||||
}
|
||||
|
||||
String _cssMapToCssStyle(Map<String, String> cssMap) {
|
||||
return cssMap.entries.fold('', (previousValue, element) {
|
||||
final kv = '${element.key}: ${element.value}';
|
||||
if (previousValue.isEmpty) {
|
||||
return kv;
|
||||
}
|
||||
return '$previousValue; $kv';
|
||||
});
|
||||
}
|
||||
// String _cssMapToCssStyle(Map<String, String> cssMap) {
|
||||
// return cssMap.entries.fold('', (previousValue, element) {
|
||||
// final kv = '${element.key}: ${element.value}';
|
||||
// if (previousValue.isEmpty) {
|
||||
// return kv;
|
||||
// }
|
||||
// return '$previousValue; $kv';
|
||||
// });
|
||||
// }
|
||||
|
||||
/// Convert the rich text to HTML
|
||||
///
|
||||
|
|
@ -565,115 +542,115 @@ class NodesToHTMLConverter {
|
|||
/// ```html
|
||||
/// <span style='...'>Text</span>
|
||||
/// ```
|
||||
html.Element _deltaToHtml(
|
||||
Delta delta, {
|
||||
String? subType,
|
||||
String? heading,
|
||||
int? end,
|
||||
bool? checked,
|
||||
}) {
|
||||
if (end != null) {
|
||||
delta = delta.slice(0, end);
|
||||
}
|
||||
// html.Element _deltaToHtml(
|
||||
// Delta delta, {
|
||||
// String? subType,
|
||||
// String? heading,
|
||||
// int? end,
|
||||
// bool? checked,
|
||||
// }) {
|
||||
// if (end != null) {
|
||||
// delta = delta.slice(0, end);
|
||||
// }
|
||||
|
||||
final childNodes = <html.Node>[];
|
||||
String tagName = HTMLTag.paragraph;
|
||||
// final childNodes = <html.Node>[];
|
||||
// String tagName = HTMLTag.paragraph;
|
||||
|
||||
if (subType == BuiltInAttributeKey.bulletedList ||
|
||||
subType == BuiltInAttributeKey.numberList) {
|
||||
tagName = HTMLTag.list;
|
||||
} else if (subType == BuiltInAttributeKey.checkbox) {
|
||||
final node = html.Element.html('<input type="checkbox" />');
|
||||
if (checked != null && checked) {
|
||||
node.attributes['checked'] = 'true';
|
||||
}
|
||||
childNodes.add(node);
|
||||
} else if (subType == BuiltInAttributeKey.heading) {
|
||||
if (heading == BuiltInAttributeKey.h1) {
|
||||
tagName = HTMLTag.h1;
|
||||
} else if (heading == BuiltInAttributeKey.h2) {
|
||||
tagName = HTMLTag.h2;
|
||||
} else if (heading == BuiltInAttributeKey.h3) {
|
||||
tagName = HTMLTag.h3;
|
||||
}
|
||||
} else if (subType == BuiltInAttributeKey.quote) {
|
||||
tagName = HTMLTag.blockQuote;
|
||||
}
|
||||
// if (subType == BuiltInAttributeKey.bulletedList ||
|
||||
// subType == BuiltInAttributeKey.numberList) {
|
||||
// tagName = HTMLTag.list;
|
||||
// } else if (subType == BuiltInAttributeKey.checkbox) {
|
||||
// final node = html.Element.html('<input type="checkbox" />');
|
||||
// if (checked != null && checked) {
|
||||
// node.attributes['checked'] = 'true';
|
||||
// }
|
||||
// childNodes.add(node);
|
||||
// } else if (subType == BuiltInAttributeKey.heading) {
|
||||
// if (heading == BuiltInAttributeKey.h1) {
|
||||
// tagName = HTMLTag.h1;
|
||||
// } else if (heading == BuiltInAttributeKey.h2) {
|
||||
// tagName = HTMLTag.h2;
|
||||
// } else if (heading == BuiltInAttributeKey.h3) {
|
||||
// tagName = HTMLTag.h3;
|
||||
// }
|
||||
// } else if (subType == BuiltInAttributeKey.quote) {
|
||||
// tagName = HTMLTag.blockQuote;
|
||||
// }
|
||||
|
||||
for (final op in delta) {
|
||||
if (op is TextInsert) {
|
||||
final attributes = op.attributes;
|
||||
if (attributes != null) {
|
||||
if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.bold] == true) {
|
||||
final strong = html.Element.tag(HTMLTag.strong);
|
||||
strong.append(html.Text(op.text));
|
||||
childNodes.add(strong);
|
||||
} else if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.underline] == true) {
|
||||
final strong = html.Element.tag(HTMLTag.underline);
|
||||
strong.append(html.Text(op.text));
|
||||
childNodes.add(strong);
|
||||
} else if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.italic] == true) {
|
||||
final strong = html.Element.tag(HTMLTag.italic);
|
||||
strong.append(html.Text(op.text));
|
||||
childNodes.add(strong);
|
||||
} else if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||
final strong = html.Element.tag(HTMLTag.del);
|
||||
strong.append(html.Text(op.text));
|
||||
childNodes.add(strong);
|
||||
} else if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.code] == true) {
|
||||
final code = html.Element.tag(HTMLTag.code);
|
||||
code.append(html.Text(op.text));
|
||||
childNodes.add(code);
|
||||
} else if (attributes.length == 1 &&
|
||||
attributes[BuiltInAttributeKey.href] != null) {
|
||||
final anchor = html.Element.tag(HTMLTag.anchor);
|
||||
anchor.attributes['href'] = attributes[BuiltInAttributeKey.href];
|
||||
anchor.append(html.Text(op.text));
|
||||
childNodes.add(anchor);
|
||||
} else {
|
||||
final span = html.Element.tag(HTMLTag.span);
|
||||
final cssString = _attributesToCssStyle(attributes);
|
||||
if (cssString.isNotEmpty) {
|
||||
span.attributes['style'] = cssString;
|
||||
}
|
||||
span.append(html.Text(op.text));
|
||||
childNodes.add(span);
|
||||
}
|
||||
} else {
|
||||
childNodes.add(html.Text(op.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
// for (final op in delta) {
|
||||
// if (op is TextInsert) {
|
||||
// final attributes = op.attributes;
|
||||
// if (attributes != null) {
|
||||
// if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.bold] == true) {
|
||||
// final strong = html.Element.tag(HTMLTag.strong);
|
||||
// strong.append(html.Text(op.text));
|
||||
// childNodes.add(strong);
|
||||
// } else if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.underline] == true) {
|
||||
// final strong = html.Element.tag(HTMLTag.underline);
|
||||
// strong.append(html.Text(op.text));
|
||||
// childNodes.add(strong);
|
||||
// } else if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.italic] == true) {
|
||||
// final strong = html.Element.tag(HTMLTag.italic);
|
||||
// strong.append(html.Text(op.text));
|
||||
// childNodes.add(strong);
|
||||
// } else if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||
// final strong = html.Element.tag(HTMLTag.del);
|
||||
// strong.append(html.Text(op.text));
|
||||
// childNodes.add(strong);
|
||||
// } else if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.code] == true) {
|
||||
// final code = html.Element.tag(HTMLTag.code);
|
||||
// code.append(html.Text(op.text));
|
||||
// childNodes.add(code);
|
||||
// } else if (attributes.length == 1 &&
|
||||
// attributes[BuiltInAttributeKey.href] != null) {
|
||||
// final anchor = html.Element.tag(HTMLTag.anchor);
|
||||
// anchor.attributes['href'] = attributes[BuiltInAttributeKey.href];
|
||||
// anchor.append(html.Text(op.text));
|
||||
// childNodes.add(anchor);
|
||||
// } else {
|
||||
// final span = html.Element.tag(HTMLTag.span);
|
||||
// final cssString = _attributesToCssStyle(attributes);
|
||||
// if (cssString.isNotEmpty) {
|
||||
// span.attributes['style'] = cssString;
|
||||
// }
|
||||
// span.append(html.Text(op.text));
|
||||
// childNodes.add(span);
|
||||
// }
|
||||
// } else {
|
||||
// childNodes.add(html.Text(op.text));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
if (tagName == HTMLTag.blockQuote) {
|
||||
final p = html.Element.tag(HTMLTag.paragraph);
|
||||
for (final node in childNodes) {
|
||||
p.append(node);
|
||||
}
|
||||
final blockQuote = html.Element.tag(tagName);
|
||||
blockQuote.append(p);
|
||||
return blockQuote;
|
||||
} else if (!HTMLTag.isTopLevel(tagName)) {
|
||||
final p = html.Element.tag(HTMLTag.paragraph);
|
||||
for (final node in childNodes) {
|
||||
p.append(node);
|
||||
}
|
||||
final result = html.Element.tag(HTMLTag.list);
|
||||
result.append(p);
|
||||
return result;
|
||||
} else {
|
||||
final p = html.Element.tag(tagName);
|
||||
for (final node in childNodes) {
|
||||
p.append(node);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
}
|
||||
// if (tagName == HTMLTag.blockQuote) {
|
||||
// final p = html.Element.tag(HTMLTag.paragraph);
|
||||
// for (final node in childNodes) {
|
||||
// p.append(node);
|
||||
// }
|
||||
// final blockQuote = html.Element.tag(tagName);
|
||||
// blockQuote.append(p);
|
||||
// return blockQuote;
|
||||
// } else if (!HTMLTag.isTopLevel(tagName)) {
|
||||
// final p = html.Element.tag(HTMLTag.paragraph);
|
||||
// for (final node in childNodes) {
|
||||
// p.append(node);
|
||||
// }
|
||||
// final result = html.Element.tag(HTMLTag.list);
|
||||
// result.append(p);
|
||||
// return result;
|
||||
// } else {
|
||||
// final p = html.Element.tag(tagName);
|
||||
// for (final node in childNodes) {
|
||||
// p.append(node);
|
||||
// }
|
||||
// return p;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
String stringify(html.Node node) {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
import 'package:appflowy_editor/src/core/document/node.dart';
|
||||
|
||||
class Infra {
|
||||
// find the forward nearest text node
|
||||
static TextNode? forwardNearestTextNode(Node node) {
|
||||
var previous = node.previous;
|
||||
while (previous != null) {
|
||||
final lastTextNode = findLastTextNode(previous);
|
||||
if (lastTextNode != null) {
|
||||
return lastTextNode;
|
||||
}
|
||||
if (previous is TextNode) {
|
||||
return previous;
|
||||
}
|
||||
previous = previous.previous;
|
||||
}
|
||||
final parent = node.parent;
|
||||
if (parent != null) {
|
||||
if (parent is TextNode) {
|
||||
return parent;
|
||||
}
|
||||
return forwardNearestTextNode(parent);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// find the last text node
|
||||
static TextNode? findLastTextNode(Node node) {
|
||||
final children = node.children.toList(growable: false).reversed;
|
||||
for (final child in children) {
|
||||
if (child.children.isNotEmpty) {
|
||||
final result = findLastTextNode(child);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (child is TextNode) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
if (node is TextNode) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Iterable<ThemeExtension<dynamic>> get lightPluginStyleExtension => [
|
||||
HeadingPluginStyle.light,
|
||||
CheckboxPluginStyle.light,
|
||||
NumberListPluginStyle.light,
|
||||
QuotedTextPluginStyle.light,
|
||||
BulletedListPluginStyle.light,
|
||||
];
|
||||
|
||||
Iterable<ThemeExtension<dynamic>> get darkPluginStyleExtension => [
|
||||
HeadingPluginStyle.dark,
|
||||
CheckboxPluginStyle.dark,
|
||||
NumberListPluginStyle.dark,
|
||||
QuotedTextPluginStyle.dark,
|
||||
BulletedListPluginStyle.dark,
|
||||
];
|
||||
|
||||
typedef TextStyleCustomizer = TextStyle Function(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
);
|
||||
typedef PaddingCustomizer = EdgeInsets Function(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
);
|
||||
typedef IconCustomizer = Widget Function(
|
||||
EditorState editorState,
|
||||
TextNode textNode,
|
||||
);
|
||||
|
||||
class HeadingPluginStyle extends ThemeExtension<HeadingPluginStyle> {
|
||||
const HeadingPluginStyle({
|
||||
required this.textStyle,
|
||||
required this.padding,
|
||||
});
|
||||
|
||||
final TextStyleCustomizer textStyle;
|
||||
final PaddingCustomizer padding;
|
||||
|
||||
@override
|
||||
HeadingPluginStyle copyWith({
|
||||
TextStyleCustomizer? textStyle,
|
||||
PaddingCustomizer? padding,
|
||||
}) {
|
||||
return HeadingPluginStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
padding: padding ?? this.padding,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<HeadingPluginStyle> lerp(
|
||||
ThemeExtension<HeadingPluginStyle>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! HeadingPluginStyle) {
|
||||
return this;
|
||||
}
|
||||
return HeadingPluginStyle(
|
||||
textStyle: other.textStyle,
|
||||
padding: other.padding,
|
||||
);
|
||||
}
|
||||
|
||||
static final light = HeadingPluginStyle(
|
||||
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: (editorState, textNode) {
|
||||
final headingToFontSize = {
|
||||
'h1': 32.0,
|
||||
'h2': 28.0,
|
||||
'h3': 24.0,
|
||||
'h4': 18.0,
|
||||
'h5': 18.0,
|
||||
'h6': 18.0,
|
||||
};
|
||||
final fontSize = headingToFontSize[textNode.attributes.heading] ?? 18.0;
|
||||
return TextStyle(
|
||||
fontSize: fontSize,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
static final dark = light;
|
||||
}
|
||||
|
||||
class CheckboxPluginStyle extends ThemeExtension<CheckboxPluginStyle> {
|
||||
const CheckboxPluginStyle({
|
||||
required this.textStyle,
|
||||
required this.padding,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final TextStyleCustomizer textStyle;
|
||||
final PaddingCustomizer padding;
|
||||
final IconCustomizer icon;
|
||||
|
||||
@override
|
||||
CheckboxPluginStyle copyWith({
|
||||
TextStyleCustomizer? textStyle,
|
||||
PaddingCustomizer? padding,
|
||||
IconCustomizer? icon,
|
||||
}) {
|
||||
return CheckboxPluginStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
padding: padding ?? this.padding,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<CheckboxPluginStyle> lerp(
|
||||
ThemeExtension<CheckboxPluginStyle>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! CheckboxPluginStyle) {
|
||||
return this;
|
||||
}
|
||||
return CheckboxPluginStyle(
|
||||
textStyle: other.textStyle,
|
||||
padding: other.padding,
|
||||
icon: other.icon,
|
||||
);
|
||||
}
|
||||
|
||||
static final light = CheckboxPluginStyle(
|
||||
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: (editorState, textNode) {
|
||||
final isCheck = textNode.attributes.check;
|
||||
return TextStyle(
|
||||
decoration: isCheck ? TextDecoration.lineThrough : null,
|
||||
color: isCheck ? Colors.grey.shade400 : null,
|
||||
);
|
||||
},
|
||||
icon: (editorState, textNode) {
|
||||
final isCheck = textNode.attributes.check;
|
||||
const iconSize = Size.square(20.0);
|
||||
const iconPadding = EdgeInsets.only(right: 5.0);
|
||||
return EditorSvg(
|
||||
width: iconSize.width,
|
||||
height: iconSize.height,
|
||||
padding: iconPadding,
|
||||
name: isCheck ? 'check' : 'uncheck',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
static final dark = light;
|
||||
}
|
||||
|
||||
class BulletedListPluginStyle extends ThemeExtension<BulletedListPluginStyle> {
|
||||
const BulletedListPluginStyle({
|
||||
required this.textStyle,
|
||||
required this.padding,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final TextStyleCustomizer textStyle;
|
||||
final PaddingCustomizer padding;
|
||||
final IconCustomizer icon;
|
||||
|
||||
@override
|
||||
BulletedListPluginStyle copyWith({
|
||||
TextStyleCustomizer? textStyle,
|
||||
PaddingCustomizer? padding,
|
||||
IconCustomizer? icon,
|
||||
}) {
|
||||
return BulletedListPluginStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
padding: padding ?? this.padding,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<BulletedListPluginStyle> lerp(
|
||||
ThemeExtension<BulletedListPluginStyle>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! BulletedListPluginStyle) {
|
||||
return this;
|
||||
}
|
||||
return BulletedListPluginStyle(
|
||||
textStyle: other.textStyle,
|
||||
padding: other.padding,
|
||||
icon: other.icon,
|
||||
);
|
||||
}
|
||||
|
||||
static final light = BulletedListPluginStyle(
|
||||
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: (_, __) => const TextStyle(),
|
||||
icon: (_, __) {
|
||||
const iconSize = Size.square(20.0);
|
||||
const iconPadding = EdgeInsets.only(right: 5.0);
|
||||
return EditorSvg(
|
||||
width: iconSize.width,
|
||||
height: iconSize.height,
|
||||
padding: iconPadding,
|
||||
color: Colors.black,
|
||||
name: 'point',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
static final dark = light.copyWith(
|
||||
icon: (_, __) {
|
||||
const iconSize = Size.square(20.0);
|
||||
const iconPadding = EdgeInsets.only(right: 5.0);
|
||||
return EditorSvg(
|
||||
width: iconSize.width,
|
||||
height: iconSize.height,
|
||||
padding: iconPadding,
|
||||
color: Colors.white,
|
||||
name: 'point',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class NumberListPluginStyle extends ThemeExtension<NumberListPluginStyle> {
|
||||
const NumberListPluginStyle({
|
||||
required this.textStyle,
|
||||
required this.padding,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final TextStyleCustomizer textStyle;
|
||||
final PaddingCustomizer padding;
|
||||
final IconCustomizer icon;
|
||||
|
||||
@override
|
||||
NumberListPluginStyle copyWith({
|
||||
TextStyleCustomizer? textStyle,
|
||||
PaddingCustomizer? padding,
|
||||
IconCustomizer? icon,
|
||||
}) {
|
||||
return NumberListPluginStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
padding: padding ?? this.padding,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<NumberListPluginStyle> lerp(
|
||||
ThemeExtension<NumberListPluginStyle>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! NumberListPluginStyle) {
|
||||
return this;
|
||||
}
|
||||
return NumberListPluginStyle(
|
||||
textStyle: other.textStyle,
|
||||
padding: other.padding,
|
||||
icon: other.icon,
|
||||
);
|
||||
}
|
||||
|
||||
static final light = NumberListPluginStyle(
|
||||
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: (_, __) => const TextStyle(),
|
||||
icon: (_, textNode) {
|
||||
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
|
||||
return Container(
|
||||
padding: iconPadding,
|
||||
child: Text(
|
||||
'${textNode.attributes.number.toString()}.',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
static final dark = light.copyWith(
|
||||
icon: (editorState, textNode) {
|
||||
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
|
||||
return Container(
|
||||
padding: iconPadding,
|
||||
child: Text(
|
||||
'${textNode.attributes.number.toString()}.',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class QuotedTextPluginStyle extends ThemeExtension<QuotedTextPluginStyle> {
|
||||
const QuotedTextPluginStyle({
|
||||
required this.textStyle,
|
||||
required this.padding,
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
final TextStyleCustomizer textStyle;
|
||||
final PaddingCustomizer padding;
|
||||
final IconCustomizer icon;
|
||||
|
||||
@override
|
||||
QuotedTextPluginStyle copyWith({
|
||||
TextStyleCustomizer? textStyle,
|
||||
PaddingCustomizer? padding,
|
||||
IconCustomizer? icon,
|
||||
}) {
|
||||
return QuotedTextPluginStyle(
|
||||
textStyle: textStyle ?? this.textStyle,
|
||||
padding: padding ?? this.padding,
|
||||
icon: icon ?? this.icon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ThemeExtension<QuotedTextPluginStyle> lerp(
|
||||
ThemeExtension<QuotedTextPluginStyle>? other,
|
||||
double t,
|
||||
) {
|
||||
if (other is! QuotedTextPluginStyle) {
|
||||
return this;
|
||||
}
|
||||
return QuotedTextPluginStyle(
|
||||
textStyle: other.textStyle,
|
||||
padding: other.padding,
|
||||
icon: other.icon,
|
||||
);
|
||||
}
|
||||
|
||||
static final light = QuotedTextPluginStyle(
|
||||
padding: (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
|
||||
textStyle: (_, __) => const TextStyle(),
|
||||
icon: (_, __) {
|
||||
const iconSize = Size.square(20.0);
|
||||
const iconPadding = EdgeInsets.only(right: 5.0);
|
||||
return EditorSvg(
|
||||
width: iconSize.width,
|
||||
padding: iconPadding,
|
||||
name: 'quote',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
static final dark = light;
|
||||
}
|
||||
|
|
@ -1,28 +1,21 @@
|
|||
import 'package:appflowy_editor/src/service/context_menu/context_menu.dart';
|
||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
||||
|
||||
final builtInContextMenuItems = [
|
||||
[
|
||||
// cut
|
||||
ContextMenuItem(
|
||||
name: 'Cut',
|
||||
onPressed: (editorState) {
|
||||
cutEventHandler(editorState, null);
|
||||
},
|
||||
onPressed: (editorState) {},
|
||||
),
|
||||
// copy
|
||||
ContextMenuItem(
|
||||
name: 'Copy',
|
||||
onPressed: (editorState) {
|
||||
copyEventHandler(editorState, null);
|
||||
},
|
||||
onPressed: (editorState) {},
|
||||
),
|
||||
// Paste
|
||||
ContextMenuItem(
|
||||
name: 'Paste',
|
||||
onPressed: (editorState) {
|
||||
pasteEventHandler(editorState, null);
|
||||
},
|
||||
onPressed: (editorState) {},
|
||||
),
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -70,147 +70,3 @@ bool insertNodeAfterSelection(
|
|||
editorState.apply(transaction);
|
||||
return true;
|
||||
}
|
||||
|
||||
void formatText(EditorState editorState) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void formatHeading(EditorState editorState, String heading) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void formatQuote(EditorState editorState) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void formatCheckbox(EditorState editorState, bool check) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
void formatBulletedList(EditorState editorState) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool formatBold(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.bold);
|
||||
}
|
||||
|
||||
bool formatItalic(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.italic);
|
||||
}
|
||||
|
||||
bool formatUnderline(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.underline);
|
||||
}
|
||||
|
||||
bool formatStrikethrough(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(
|
||||
editorState,
|
||||
BuiltInAttributeKey.strikethrough,
|
||||
);
|
||||
}
|
||||
|
||||
bool formatEmbedCode(EditorState editorState) {
|
||||
return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.code);
|
||||
}
|
||||
|
||||
bool formatHighlight(EditorState editorState, String colorHex) {
|
||||
bool value = _allSatisfyInSelection(
|
||||
editorState,
|
||||
BuiltInAttributeKey.highlightColor,
|
||||
colorHex,
|
||||
);
|
||||
return formatRichTextPartialStyle(
|
||||
editorState,
|
||||
BuiltInAttributeKey.highlightColor,
|
||||
customValue: value ? '0x00000000' : colorHex,
|
||||
);
|
||||
}
|
||||
|
||||
bool formatRichTextPartialStyle(
|
||||
EditorState editorState,
|
||||
String styleKey, {
|
||||
Object? customValue,
|
||||
}) {
|
||||
Attributes attributes = {
|
||||
styleKey: customValue ??
|
||||
!_allSatisfyInSelection(
|
||||
editorState,
|
||||
styleKey,
|
||||
customValue ?? true,
|
||||
),
|
||||
};
|
||||
|
||||
return formatRichTextStyle(editorState, attributes);
|
||||
}
|
||||
|
||||
bool _allSatisfyInSelection(
|
||||
EditorState editorState,
|
||||
String styleKey,
|
||||
dynamic matchValue,
|
||||
) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final textNodes = nodes.whereType<TextNode>().toList(growable: false);
|
||||
|
||||
if (selection == null || textNodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return textNodes.allSatisfyInSelection(selection, styleKey, (value) {
|
||||
return value == matchValue;
|
||||
});
|
||||
}
|
||||
|
||||
bool formatRichTextStyle(EditorState editorState, Attributes attributes) {
|
||||
var selection = editorState.service.selectionService.currentSelection.value;
|
||||
var nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nodes = selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
|
||||
selection = selection.isBackward ? selection : selection.reversed;
|
||||
|
||||
var textNodes = nodes.whereType<TextNode>().toList();
|
||||
if (textNodes.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final transaction = editorState.transaction;
|
||||
|
||||
// 1. All nodes are text nodes.
|
||||
// 2. The first node is not TextNode.
|
||||
// 3. The last node is not TextNode.
|
||||
if (nodes.length == textNodes.length && textNodes.length == 1) {
|
||||
transaction.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];
|
||||
var index = 0;
|
||||
var length = textNode.toPlainText().length;
|
||||
if (i == 0 && textNode == nodes.first) {
|
||||
index = selection.start.offset;
|
||||
length = textNode.toPlainText().length - selection.start.offset;
|
||||
} else if (i == textNodes.length - 1 && textNode == nodes.last) {
|
||||
length = selection.end.offset;
|
||||
}
|
||||
transaction.formatText(
|
||||
textNode,
|
||||
index,
|
||||
length,
|
||||
attributes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
editorState.apply(transaction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
ShortcutEventHandler toggleCheckbox = (editorState, event) {
|
||||
final selection = editorState.service.selectionService.currentSelection.value;
|
||||
final nodes = editorState.service.selectionService.currentSelectedNodes;
|
||||
final checkboxTextNodes = nodes
|
||||
.where(
|
||||
(element) =>
|
||||
element is TextNode &&
|
||||
element.subtype == BuiltInAttributeKey.checkbox,
|
||||
)
|
||||
.toList(growable: false);
|
||||
|
||||
if (selection == null || checkboxTextNodes.isEmpty) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
|
||||
bool isAllCheckboxesChecked = checkboxTextNodes
|
||||
.every((node) => node.attributes[BuiltInAttributeKey.checkbox] == true);
|
||||
final transaction = editorState.transaction;
|
||||
transaction.afterSelection = selection;
|
||||
|
||||
if (isAllCheckboxesChecked) {
|
||||
//if all the checkboxes are checked, then make all of the checkboxes unchecked
|
||||
for (final node in checkboxTextNodes) {
|
||||
transaction.updateNode(node, {BuiltInAttributeKey.checkbox: false});
|
||||
}
|
||||
} else {
|
||||
//If any one of the checkboxes is unchecked then make all checkboxes checked
|
||||
for (final node in checkboxTextNodes) {
|
||||
transaction.updateNode(node, {BuiltInAttributeKey.checkbox: true});
|
||||
}
|
||||
}
|
||||
|
||||
editorState.apply(transaction);
|
||||
return KeyEventResult.handled;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue