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
|
* [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)
|
* 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
|
## Glossary
|
||||||
Please refer to the API documentation.
|
Please refer to the API documentation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,5 @@ linter:
|
||||||
- use_decorated_box
|
- use_decorated_box
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
errors:
|
|
||||||
deprecated_member_use_from_same_package: ignore
|
|
||||||
exclude:
|
exclude:
|
||||||
- lib/src/l10n/**
|
- 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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the [TextNode] at the given [Path]
|
/// Updates the [Node] with [Delta] at the given [Path]
|
||||||
bool updateText(Path path, Delta delta) {
|
bool updateText(Path path, Delta delta) {
|
||||||
if (path.isEmpty) {
|
if (path.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final target = nodeAtPath(path);
|
final target = nodeAtPath(path);
|
||||||
if (target == null || target is! TextNode) {
|
final targetDelta = target?.delta;
|
||||||
|
if (target == null || targetDelta == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
target.delta = target.delta.compose(delta);
|
target.updateAttributes({'delta': (targetDelta.compose(delta)).toJson()});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -125,12 +125,6 @@ class Transaction {
|
||||||
insertNode(path, node, deepCopy: false);
|
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.
|
/// Returns the JSON representation of the transaction.
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
|
@ -551,126 +545,6 @@ extension TextTransaction on Transaction {
|
||||||
markNeedsComposing = true;
|
markNeedsComposing = true;
|
||||||
_composeMap.putIfAbsent(node, () => []).add(delta);
|
_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 {
|
extension on Delta {
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,12 @@ Node paragraphNode({
|
||||||
Attributes? attributes,
|
Attributes? attributes,
|
||||||
Iterable<Node> children = const [],
|
Iterable<Node> children = const [],
|
||||||
}) {
|
}) {
|
||||||
attributes ??= {
|
|
||||||
ParagraphBlockKeys.delta: (delta ?? (Delta()..insert(text ?? ''))).toJson(),
|
|
||||||
};
|
|
||||||
return Node(
|
return Node(
|
||||||
type: ParagraphBlockKeys.type,
|
type: ParagraphBlockKeys.type,
|
||||||
attributes: {
|
attributes: {
|
||||||
...attributes,
|
ParagraphBlockKeys.delta:
|
||||||
|
(delta ?? (Delta()..insert(text ?? ''))).toJson(),
|
||||||
|
if (attributes != null) ...attributes,
|
||||||
if (textDirection != null)
|
if (textDirection != null)
|
||||||
ParagraphBlockKeys.textDirection: textDirection,
|
ParagraphBlockKeys.textDirection: textDirection,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -289,7 +289,7 @@ class _DesktopSelectionServiceWidgetState
|
||||||
final selection = currentSelection.value;
|
final selection = currentSelection.value;
|
||||||
if (selection == null ||
|
if (selection == null ||
|
||||||
(selection.isCollapsed == true &&
|
(selection.isCollapsed == true &&
|
||||||
currentSelectedNodes.first is TextNode)) {
|
currentSelectedNodes.first.delta != null)) {
|
||||||
_onDoubleTapDown(details);
|
_onDoubleTapDown(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,7 +529,7 @@ class _DesktopSelectionServiceWidgetState
|
||||||
_clearContextMenu();
|
_clearContextMenu();
|
||||||
|
|
||||||
// For now, only support the text node.
|
// For now, only support the text node.
|
||||||
if (!currentSelectedNodes.every((element) => element is TextNode)) {
|
if (!currentSelectedNodes.every((element) => element.delta != null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,15 @@ CommandShortcutEventHandler _copyCommandHandler = (editorState) {
|
||||||
|
|
||||||
// rich text.
|
// rich text.
|
||||||
// TODO: support rich text. the below code is not working.
|
// TODO: support rich text. the below code is not working.
|
||||||
final nodes = editorState.getNodesInSelection(selection);
|
// final nodes = editorState.getNodesInSelection(selection);
|
||||||
final html = NodesToHTMLConverter(
|
// final html = NodesToHTMLConverter(
|
||||||
nodes: nodes,
|
// nodes: nodes,
|
||||||
startOffset: selection.startIndex,
|
// startOffset: selection.startIndex,
|
||||||
endOffset: selection.endIndex,
|
// endOffset: selection.endIndex,
|
||||||
).toHTMLString();
|
// ).toHTMLString();
|
||||||
|
|
||||||
AppFlowyClipboard.setData(
|
AppFlowyClipboard.setData(
|
||||||
text: text,
|
text: text,
|
||||||
html: html.isEmpty ? null : html,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return KeyEventResult.handled;
|
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 'attributes_extension.dart';
|
||||||
export 'color_extension.dart';
|
export 'color_extension.dart';
|
||||||
export 'editor_state_extensions.dart';
|
|
||||||
export 'node_extensions.dart';
|
export 'node_extensions.dart';
|
||||||
export 'object_extensions.dart';
|
export 'object_extensions.dart';
|
||||||
export 'position_extension.dart';
|
export 'position_extension.dart';
|
||||||
export 'text_node_extensions.dart';
|
|
||||||
export 'text_style_extension.dart';
|
export 'text_style_extension.dart';
|
||||||
export 'url_launcher_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:collection';
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:html/parser.dart' show parse;
|
import 'package:html/parser.dart' show parse;
|
||||||
import 'package:html/dom.dart' as html;
|
import 'package:html/dom.dart' as html;
|
||||||
|
|
||||||
|
|
@ -400,7 +398,7 @@ class NodesToHTMLConverter {
|
||||||
/// </ul>
|
/// </ul>
|
||||||
///
|
///
|
||||||
/// This container is used to save the list elements temporarily.
|
/// This container is used to save the list elements temporarily.
|
||||||
html.Element? _stashListContainer;
|
// html.Element? _stashListContainer;
|
||||||
|
|
||||||
NodesToHTMLConverter({
|
NodesToHTMLConverter({
|
||||||
required this.nodes,
|
required this.nodes,
|
||||||
|
|
@ -411,72 +409,63 @@ class NodesToHTMLConverter {
|
||||||
return;
|
return;
|
||||||
} else if (nodes.length == 1) {
|
} else if (nodes.length == 1) {
|
||||||
final first = nodes.first;
|
final first = nodes.first;
|
||||||
if (first is TextNode) {
|
if (first.delta != null) {
|
||||||
nodes[0] = first.copyWith(
|
nodes[0] = first.copyWith(
|
||||||
delta: first.delta.slice(startOffset ?? 0, endOffset),
|
attributes: {
|
||||||
|
...first.attributes,
|
||||||
|
'delta': (first.delta!.slice(startOffset ?? 0, endOffset)).toJson(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final first = nodes.first;
|
final first = nodes.first;
|
||||||
final last = nodes.last;
|
final last = nodes.last;
|
||||||
if (first is TextNode) {
|
if (first.delta != null) {
|
||||||
nodes[0] = first.copyWith(delta: first.delta.slice(startOffset ?? 0));
|
nodes[0] = first.copyWith(
|
||||||
|
attributes: {
|
||||||
|
...first.attributes,
|
||||||
|
'delta': (first.delta!.slice(startOffset ?? 0)).toJson(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (last is TextNode) {
|
if (last.delta != null) {
|
||||||
nodes[nodes.length - 1] =
|
nodes[nodes.length - 1] = first.copyWith(
|
||||||
last.copyWith(delta: last.delta.slice(0, endOffset));
|
attributes: {
|
||||||
|
...first.attributes,
|
||||||
|
'delta': (last.delta!.slice(0, endOffset)).toJson(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<html.Node> toHTMLNodes() {
|
List<html.Node> toHTMLNodes() {
|
||||||
for (final node in nodes) {
|
// for (final node in nodes) {
|
||||||
if (node.type == 'text') {
|
// if (node.type == 'text') {
|
||||||
final textNode = node as TextNode;
|
// final textNode = node as TextNode;
|
||||||
if (node == nodes.first) {
|
// if (node == nodes.first) {
|
||||||
_addTextNode(textNode);
|
// _addTextNode(textNode);
|
||||||
} else if (node == nodes.last) {
|
// } else if (node == nodes.last) {
|
||||||
_addTextNode(textNode, end: endOffset);
|
// _addTextNode(textNode, end: endOffset);
|
||||||
} else {
|
// } else {
|
||||||
_addTextNode(textNode);
|
// _addTextNode(textNode);
|
||||||
}
|
// }
|
||||||
} else if (node.type == 'image') {
|
// } else if (node.type == 'image') {
|
||||||
final textNode = node;
|
// final textNode = node;
|
||||||
final anchor = html.Element.tag(HTMLTag.image);
|
// final anchor = html.Element.tag(HTMLTag.image);
|
||||||
anchor.attributes['src'] = textNode.attributes['image_src'];
|
// anchor.attributes['src'] = textNode.attributes['image_src'];
|
||||||
|
|
||||||
_result.add(anchor);
|
// _result.add(anchor);
|
||||||
}
|
// }
|
||||||
// TODO: handle image and other blocks
|
// // TODO: handle image and other blocks
|
||||||
}
|
// }
|
||||||
if (_stashListContainer != null) {
|
// if (_stashListContainer != null) {
|
||||||
_result.add(_stashListContainer!);
|
// _result.add(_stashListContainer!);
|
||||||
_stashListContainer = null;
|
// _stashListContainer = null;
|
||||||
}
|
// }
|
||||||
return _result;
|
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() {
|
String toHTMLString() {
|
||||||
final elements = toHTMLNodes();
|
final elements = toHTMLNodes();
|
||||||
final copyString = elements.fold<String>(
|
final copyString = elements.fold<String>(
|
||||||
|
|
@ -486,68 +475,56 @@ class NodesToHTMLConverter {
|
||||||
return copyString;
|
return copyString;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.Element _textNodeToHtml(TextNode textNode, {int? end}) {
|
// String _textDecorationsFromAttributes(Attributes attributes) {
|
||||||
String? subType = textNode.attributes['subtype'];
|
// var textDecoration = <String>[];
|
||||||
String? heading = textNode.attributes['heading'];
|
// if (attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||||
return _deltaToHtml(
|
// textDecoration.add('line-through');
|
||||||
textNode.delta,
|
// }
|
||||||
subType: subType,
|
// if (attributes[BuiltInAttributeKey.underline] == true) {
|
||||||
heading: heading,
|
// textDecoration.add('underline');
|
||||||
end: end,
|
// }
|
||||||
checked: textNode.attributes['checkbox'] == true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _textDecorationsFromAttributes(Attributes attributes) {
|
// return textDecoration.join(' ');
|
||||||
var textDecoration = <String>[];
|
// }
|
||||||
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
|
|
||||||
textDecoration.add('line-through');
|
|
||||||
}
|
|
||||||
if (attributes[BuiltInAttributeKey.underline] == true) {
|
|
||||||
textDecoration.add('underline');
|
|
||||||
}
|
|
||||||
|
|
||||||
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 textDecoration = _textDecorationsFromAttributes(attributes);
|
||||||
final cssMap = <String, String>{};
|
// if (textDecoration.isNotEmpty) {
|
||||||
if (attributes[BuiltInAttributeKey.highlightColor] != null) {
|
// cssMap['text-decoration'] = textDecoration;
|
||||||
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 (attributes[BuiltInAttributeKey.italic] == true) {
|
||||||
if (textDecoration.isNotEmpty) {
|
// cssMap['font-style'] = 'italic';
|
||||||
cssMap['text-decoration'] = textDecoration;
|
// }
|
||||||
}
|
// return _cssMapToCssStyle(cssMap);
|
||||||
|
// }
|
||||||
|
|
||||||
if (attributes[BuiltInAttributeKey.italic] == true) {
|
// String _cssMapToCssStyle(Map<String, String> cssMap) {
|
||||||
cssMap['font-style'] = 'italic';
|
// return cssMap.entries.fold('', (previousValue, element) {
|
||||||
}
|
// final kv = '${element.key}: ${element.value}';
|
||||||
return _cssMapToCssStyle(cssMap);
|
// if (previousValue.isEmpty) {
|
||||||
}
|
// return kv;
|
||||||
|
// }
|
||||||
String _cssMapToCssStyle(Map<String, String> cssMap) {
|
// return '$previousValue; $kv';
|
||||||
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
|
/// Convert the rich text to HTML
|
||||||
///
|
///
|
||||||
|
|
@ -565,115 +542,115 @@ class NodesToHTMLConverter {
|
||||||
/// ```html
|
/// ```html
|
||||||
/// <span style='...'>Text</span>
|
/// <span style='...'>Text</span>
|
||||||
/// ```
|
/// ```
|
||||||
html.Element _deltaToHtml(
|
// html.Element _deltaToHtml(
|
||||||
Delta delta, {
|
// Delta delta, {
|
||||||
String? subType,
|
// String? subType,
|
||||||
String? heading,
|
// String? heading,
|
||||||
int? end,
|
// int? end,
|
||||||
bool? checked,
|
// bool? checked,
|
||||||
}) {
|
// }) {
|
||||||
if (end != null) {
|
// if (end != null) {
|
||||||
delta = delta.slice(0, end);
|
// delta = delta.slice(0, end);
|
||||||
}
|
// }
|
||||||
|
|
||||||
final childNodes = <html.Node>[];
|
// final childNodes = <html.Node>[];
|
||||||
String tagName = HTMLTag.paragraph;
|
// String tagName = HTMLTag.paragraph;
|
||||||
|
|
||||||
if (subType == BuiltInAttributeKey.bulletedList ||
|
// if (subType == BuiltInAttributeKey.bulletedList ||
|
||||||
subType == BuiltInAttributeKey.numberList) {
|
// subType == BuiltInAttributeKey.numberList) {
|
||||||
tagName = HTMLTag.list;
|
// tagName = HTMLTag.list;
|
||||||
} else if (subType == BuiltInAttributeKey.checkbox) {
|
// } else if (subType == BuiltInAttributeKey.checkbox) {
|
||||||
final node = html.Element.html('<input type="checkbox" />');
|
// final node = html.Element.html('<input type="checkbox" />');
|
||||||
if (checked != null && checked) {
|
// if (checked != null && checked) {
|
||||||
node.attributes['checked'] = 'true';
|
// node.attributes['checked'] = 'true';
|
||||||
}
|
// }
|
||||||
childNodes.add(node);
|
// childNodes.add(node);
|
||||||
} else if (subType == BuiltInAttributeKey.heading) {
|
// } else if (subType == BuiltInAttributeKey.heading) {
|
||||||
if (heading == BuiltInAttributeKey.h1) {
|
// if (heading == BuiltInAttributeKey.h1) {
|
||||||
tagName = HTMLTag.h1;
|
// tagName = HTMLTag.h1;
|
||||||
} else if (heading == BuiltInAttributeKey.h2) {
|
// } else if (heading == BuiltInAttributeKey.h2) {
|
||||||
tagName = HTMLTag.h2;
|
// tagName = HTMLTag.h2;
|
||||||
} else if (heading == BuiltInAttributeKey.h3) {
|
// } else if (heading == BuiltInAttributeKey.h3) {
|
||||||
tagName = HTMLTag.h3;
|
// tagName = HTMLTag.h3;
|
||||||
}
|
// }
|
||||||
} else if (subType == BuiltInAttributeKey.quote) {
|
// } else if (subType == BuiltInAttributeKey.quote) {
|
||||||
tagName = HTMLTag.blockQuote;
|
// tagName = HTMLTag.blockQuote;
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (final op in delta) {
|
// for (final op in delta) {
|
||||||
if (op is TextInsert) {
|
// if (op is TextInsert) {
|
||||||
final attributes = op.attributes;
|
// final attributes = op.attributes;
|
||||||
if (attributes != null) {
|
// if (attributes != null) {
|
||||||
if (attributes.length == 1 &&
|
// if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.bold] == true) {
|
// attributes[BuiltInAttributeKey.bold] == true) {
|
||||||
final strong = html.Element.tag(HTMLTag.strong);
|
// final strong = html.Element.tag(HTMLTag.strong);
|
||||||
strong.append(html.Text(op.text));
|
// strong.append(html.Text(op.text));
|
||||||
childNodes.add(strong);
|
// childNodes.add(strong);
|
||||||
} else if (attributes.length == 1 &&
|
// } else if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.underline] == true) {
|
// attributes[BuiltInAttributeKey.underline] == true) {
|
||||||
final strong = html.Element.tag(HTMLTag.underline);
|
// final strong = html.Element.tag(HTMLTag.underline);
|
||||||
strong.append(html.Text(op.text));
|
// strong.append(html.Text(op.text));
|
||||||
childNodes.add(strong);
|
// childNodes.add(strong);
|
||||||
} else if (attributes.length == 1 &&
|
// } else if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.italic] == true) {
|
// attributes[BuiltInAttributeKey.italic] == true) {
|
||||||
final strong = html.Element.tag(HTMLTag.italic);
|
// final strong = html.Element.tag(HTMLTag.italic);
|
||||||
strong.append(html.Text(op.text));
|
// strong.append(html.Text(op.text));
|
||||||
childNodes.add(strong);
|
// childNodes.add(strong);
|
||||||
} else if (attributes.length == 1 &&
|
// } else if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.strikethrough] == true) {
|
// attributes[BuiltInAttributeKey.strikethrough] == true) {
|
||||||
final strong = html.Element.tag(HTMLTag.del);
|
// final strong = html.Element.tag(HTMLTag.del);
|
||||||
strong.append(html.Text(op.text));
|
// strong.append(html.Text(op.text));
|
||||||
childNodes.add(strong);
|
// childNodes.add(strong);
|
||||||
} else if (attributes.length == 1 &&
|
// } else if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.code] == true) {
|
// attributes[BuiltInAttributeKey.code] == true) {
|
||||||
final code = html.Element.tag(HTMLTag.code);
|
// final code = html.Element.tag(HTMLTag.code);
|
||||||
code.append(html.Text(op.text));
|
// code.append(html.Text(op.text));
|
||||||
childNodes.add(code);
|
// childNodes.add(code);
|
||||||
} else if (attributes.length == 1 &&
|
// } else if (attributes.length == 1 &&
|
||||||
attributes[BuiltInAttributeKey.href] != null) {
|
// attributes[BuiltInAttributeKey.href] != null) {
|
||||||
final anchor = html.Element.tag(HTMLTag.anchor);
|
// final anchor = html.Element.tag(HTMLTag.anchor);
|
||||||
anchor.attributes['href'] = attributes[BuiltInAttributeKey.href];
|
// anchor.attributes['href'] = attributes[BuiltInAttributeKey.href];
|
||||||
anchor.append(html.Text(op.text));
|
// anchor.append(html.Text(op.text));
|
||||||
childNodes.add(anchor);
|
// childNodes.add(anchor);
|
||||||
} else {
|
// } else {
|
||||||
final span = html.Element.tag(HTMLTag.span);
|
// final span = html.Element.tag(HTMLTag.span);
|
||||||
final cssString = _attributesToCssStyle(attributes);
|
// final cssString = _attributesToCssStyle(attributes);
|
||||||
if (cssString.isNotEmpty) {
|
// if (cssString.isNotEmpty) {
|
||||||
span.attributes['style'] = cssString;
|
// span.attributes['style'] = cssString;
|
||||||
}
|
// }
|
||||||
span.append(html.Text(op.text));
|
// span.append(html.Text(op.text));
|
||||||
childNodes.add(span);
|
// childNodes.add(span);
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
childNodes.add(html.Text(op.text));
|
// childNodes.add(html.Text(op.text));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (tagName == HTMLTag.blockQuote) {
|
// if (tagName == HTMLTag.blockQuote) {
|
||||||
final p = html.Element.tag(HTMLTag.paragraph);
|
// final p = html.Element.tag(HTMLTag.paragraph);
|
||||||
for (final node in childNodes) {
|
// for (final node in childNodes) {
|
||||||
p.append(node);
|
// p.append(node);
|
||||||
}
|
// }
|
||||||
final blockQuote = html.Element.tag(tagName);
|
// final blockQuote = html.Element.tag(tagName);
|
||||||
blockQuote.append(p);
|
// blockQuote.append(p);
|
||||||
return blockQuote;
|
// return blockQuote;
|
||||||
} else if (!HTMLTag.isTopLevel(tagName)) {
|
// } else if (!HTMLTag.isTopLevel(tagName)) {
|
||||||
final p = html.Element.tag(HTMLTag.paragraph);
|
// final p = html.Element.tag(HTMLTag.paragraph);
|
||||||
for (final node in childNodes) {
|
// for (final node in childNodes) {
|
||||||
p.append(node);
|
// p.append(node);
|
||||||
}
|
// }
|
||||||
final result = html.Element.tag(HTMLTag.list);
|
// final result = html.Element.tag(HTMLTag.list);
|
||||||
result.append(p);
|
// result.append(p);
|
||||||
return result;
|
// return result;
|
||||||
} else {
|
// } else {
|
||||||
final p = html.Element.tag(tagName);
|
// final p = html.Element.tag(tagName);
|
||||||
for (final node in childNodes) {
|
// for (final node in childNodes) {
|
||||||
p.append(node);
|
// p.append(node);
|
||||||
}
|
// }
|
||||||
return p;
|
// return p;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
String stringify(html.Node node) {
|
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/context_menu/context_menu.dart';
|
||||||
import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
|
|
||||||
|
|
||||||
final builtInContextMenuItems = [
|
final builtInContextMenuItems = [
|
||||||
[
|
[
|
||||||
// cut
|
// cut
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
name: 'Cut',
|
name: 'Cut',
|
||||||
onPressed: (editorState) {
|
onPressed: (editorState) {},
|
||||||
cutEventHandler(editorState, null);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
// copy
|
// copy
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
name: 'Copy',
|
name: 'Copy',
|
||||||
onPressed: (editorState) {
|
onPressed: (editorState) {},
|
||||||
copyEventHandler(editorState, null);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
// Paste
|
// Paste
|
||||||
ContextMenuItem(
|
ContextMenuItem(
|
||||||
name: 'Paste',
|
name: 'Paste',
|
||||||
onPressed: (editorState) {
|
onPressed: (editorState) {},
|
||||||
pasteEventHandler(editorState, null);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -70,147 +70,3 @@ bool insertNodeAfterSelection(
|
||||||
editorState.apply(transaction);
|
editorState.apply(transaction);
|
||||||
return true;
|
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